From 179887b8d881f22c5276b303d4ef81bd4314dd92 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sat, 7 Feb 2026 15:12:50 +0100 Subject: [PATCH 01/32] Add first diffing draft --- gradle/libs.versions.toml | 4 + jsontree/build.gradle.kts | 2 + .../sebastianneubauer/jsontree/JsonTree.kt | 5 + .../jsontree/JsonTreeDiff.kt | 372 ++++++++++++++++++ .../jsontree/JsonTreeDiffer2.kt | 196 +++++++++ sample/src/commonMain/kotlin/App.kt | 5 + 6 files changed, 584 insertions(+) create mode 100644 jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff.kt create mode 100644 jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8fa1f6f..e776467 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,8 @@ androidx-compose-ui-test = "1.10.1" kotlinx-serialization-json = "1.10.0" kotlinx-coroutines = "1.10.2" detekt = "1.23.8" +kotlindiff = "1.3.0" +html-converter = "1.1.0" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -35,6 +37,8 @@ jb-compose-material3 = { module = "org.jetbrains.compose.material3:material3", v kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } androidx-compose-ui-test-android = { module = "androidx.compose.ui:ui-test-junit4-android", version.ref = "androidx-compose-ui-test" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-compose-ui-test" } +kotlindiff = { module = "io.github.petertrr:kotlin-multiplatform-diff", version.ref = "kotlindiff" } +html-converter = { module = "be.digitalia.compose.htmlconverter:htmlconverter", version.ref = "html-converter" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" } diff --git a/jsontree/build.gradle.kts b/jsontree/build.gradle.kts index 3411eff..124ff78 100644 --- a/jsontree/build.gradle.kts +++ b/jsontree/build.gradle.kts @@ -52,6 +52,8 @@ kotlin { implementation(libs.kotlinx.serialization.json) // needs to be added as a workaround not get atomicfus code stripped implementation(libs.atomicfu) + implementation(libs.kotlindiff) + implementation(libs.html.converter) } commonTest.dependencies { diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt index 657b151..dcb6ff9 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt @@ -99,6 +99,11 @@ public fun JsonTree( jsonParser.init(initialState) } + LaunchedEffect(Unit) { + val differ = JsonTreeDiffer2() + differ.diff(original = differ.original, revised = differ.revised) + } + when (val state = jsonParser.state.value) { is JsonTreeParserState.Ready -> { Box(modifier = modifier) { diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff.kt new file mode 100644 index 0000000..be6a903 --- /dev/null +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff.kt @@ -0,0 +1,372 @@ +package com.sebastianneubauer.jsontree + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import be.digitalia.compose.htmlconverter.htmlToAnnotatedString +import io.github.petertrr.diffutils.text.DiffLineNormalizer +import io.github.petertrr.diffutils.text.DiffRow +import io.github.petertrr.diffutils.text.DiffRowGenerator +import io.github.petertrr.diffutils.text.DiffTagGenerator +import jsontree.jsontree.generated.resources.Res +import jsontree.jsontree.generated.resources.jsontree_arrow_right +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable.start +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.vectorResource + +@Composable +public fun JsonTreeDiff( + originalJson: String, + revisedJson: String, + onLoading: @Composable () -> Unit, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), + colors: TreeColors = defaultLightColors, + icon: ImageVector = vectorResource(Res.drawable.jsontree_arrow_right), + iconSize: Dp = 20.dp, + textStyle: TextStyle = LocalTextStyle.current, + showIndices: Boolean = false, + showItemCount: Boolean = true, + lazyListState: LazyListState = rememberLazyListState(), + onError: (Throwable) -> Unit = {} +) { + + val originalJsonParser = remember(originalJson) { + JsonTreeParser( + json = originalJson, + defaultDispatcher = Dispatchers.Default, + mainDispatcher = Dispatchers.Main + ) + } + + val revisedJsonParser = remember(revisedJson) { + JsonTreeParser( + json = originalJson, + defaultDispatcher = Dispatchers.Default, + mainDispatcher = Dispatchers.Main + ) + } + + LaunchedEffect(originalJsonParser) { + originalJsonParser.init(TreeState.EXPANDED) + } + + LaunchedEffect(revisedJsonParser) { + revisedJsonParser.init(TreeState.EXPANDED) + } + + val originalJsonParserState = originalJsonParser.state.value + val revisedJsonParserState = revisedJsonParser.state.value + + when { + originalJsonParserState is JsonTreeParserState.Ready + && revisedJsonParserState is JsonTreeParserState.Ready -> { + + } + originalJsonParserState is JsonTreeParserState.Loading + || revisedJsonParserState is JsonTreeParserState.Loading -> { + onLoading() + } + originalJsonParserState is JsonTreeParserState.Parsing.Error + || revisedJsonParserState is JsonTreeParserState.Parsing.Error -> { + val originalError = (originalJsonParserState as? JsonTreeParserState.Parsing.Error)?.throwable + val revisedError = (revisedJsonParserState as? JsonTreeParserState.Parsing.Error)?.throwable + onError(originalError ?: revisedError!!) + } + else -> error("Unexpected states $originalJsonParserState, $revisedJsonParserState") + } + +// when (val state = revisedJsonParser.state.value) { +// is JsonTreeParserState.Ready -> { +// Box(modifier = modifier) { +// JsonTreeList( +// state = state, +// contentPadding = contentPadding, +// colors = colors, +// icon = icon, +// iconSize = iconSize, +// textStyle = textStyle, +// showIndices = showIndices, +// showItemCount = showItemCount, +// searchResult = SearchState.SearchResult( +// query = null, +// occurrences = emptyMap(), +// selectedOccurrence = null, +// totalResults = 0, +// selectedResultIndex = null +// ), +// lazyListState = lazyListState, +// onClick = {} //noop +// ) +// +// } +// } +// is JsonTreeParserState.Loading -> onLoading() +// is JsonTreeParserState.Parsing.Error -> onError(state.throwable) +// is JsonTreeParserState.Parsing.Parsed -> error("Unexpected state $state") +// } +} + +@Composable +public fun SideBySideDiff() { + val original = """ + { + "topLevelObject": { + "string": "stringValue", + "nestedObject": { + "int": 42, + "nestedArray": [ + "nestedArrayValue", + "nestedArrayValue" + ], + "arrayOfObjects": [ + { + "anotherString": "anotherStringValue" + }, + { + "anotherInt": 52 + } + ] + } + }, + "topLevelArray": [ + "hello", + "world" + ], + "emptyObject": { + + } + } + """.trimIndent() + + val revised = """ + { + "topLevelObject": { + "string": "stringValue", + "nestedObject": { + "nestedArray": [ + "nestedArrayValue", + "nestedArrayValue" + ], + "rrayOfa": [ + { + "anotherString": "anotherStringValue" + }, + { + "anotherInt": 52 + }, + { + "anotherFloat": 5.0 + } + ] + } + }, + "topLevelArray": [ + "hello", + "world" + ], + "emptyObject": { + + } + } + """.trimIndent() + + val diffRows = DiffRowGenerator( + showInlineDiffs = true, + newTag = object : DiffTagGenerator { + override fun generateClose(tag: DiffRow.Tag): String { + return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" + } + + override fun generateOpen(tag: DiffRow.Tag): String { + return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" + } + }, + oldTag = object : DiffTagGenerator { + override fun generateClose(tag: DiffRow.Tag): String { + return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" + } + + override fun generateOpen(tag: DiffRow.Tag): String { + return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" + } + }, + ).generateDiffRows(original.lines(), revised.lines()) + + println(diffRows) + + val originalListState = rememberLazyListState() + val revisedListState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() + + SyncScrolling( + originalListState = originalListState, + revisedListState = revisedListState, + coroutineScope = coroutineScope + ) + + Row(modifier = Modifier.fillMaxWidth()) { + LazyColumn( + modifier = Modifier.weight(1f), + state = originalListState + ) { + items(diffRows) { diffRow -> + val text = when(diffRow.tag) { + DiffRow.Tag.INSERT -> "" + DiffRow.Tag.DELETE -> diffRow.oldLine + DiffRow.Tag.CHANGE -> diffRow.oldLine + DiffRow.Tag.EQUAL -> diffRow.oldLine + } + val strippedText = text.replace("", "").replace("", "") + val annotatedText = buildAnnotatedString { + append(strippedText) + if(diffRow.tag == DiffRow.Tag.CHANGE) { + val indices = text.findBoldTagIndicesStripped() + indices.forEach { (start, end) -> + addStyle( + style = SpanStyle(background = Color.Blue), + start = start, + end = end + ) + } + } + } + + Text( + modifier = Modifier + .fillMaxWidth() + .background( + color = when(diffRow.tag) { + DiffRow.Tag.INSERT -> Color.Transparent + DiffRow.Tag.DELETE -> Color.Red + DiffRow.Tag.CHANGE -> Color.Red + DiffRow.Tag.EQUAL -> Color.Transparent + } + ), + text = annotatedText + ) + } + } + + LazyColumn( + modifier = Modifier.weight(1f), + state = revisedListState + ) { + items(diffRows) { diffRow -> + val text = when(diffRow.tag) { + DiffRow.Tag.INSERT -> diffRow.newLine + DiffRow.Tag.DELETE -> "" + DiffRow.Tag.CHANGE -> diffRow.newLine + DiffRow.Tag.EQUAL -> diffRow.newLine + } + val strippedText = text.replace("", "").replace("", "") + val annotatedText = buildAnnotatedString { + append(strippedText) + if(diffRow.tag == DiffRow.Tag.CHANGE) { + val indices = text.findBoldTagIndicesStripped() + indices.forEach { (start, end) -> + addStyle( + style = SpanStyle(background = Color.Blue), + start = start, + end = end + ) + } + } + } + Text( + modifier = Modifier + .fillMaxWidth() + .background( + color = when(diffRow.tag) { + DiffRow.Tag.INSERT -> Color.Green + DiffRow.Tag.DELETE -> Color.Transparent + DiffRow.Tag.CHANGE -> Color.Green + DiffRow.Tag.EQUAL -> Color.Transparent + } + ), + text = annotatedText + ) + } + } + } +} + +internal fun String.findBoldTagIndicesStripped(): List> { + val result = mutableListOf>() + var strippedIndex = 0 + var currentIndex = 0 + + while (currentIndex < length) { + val openTagIndex = indexOf("", currentIndex) + if (openTagIndex == -1) break + + // Add non-bold text length to stripped index + strippedIndex += (openTagIndex - currentIndex) + + val contentStart = openTagIndex + 3 + val closeTagIndex = indexOf("", contentStart) + + if (closeTagIndex == -1) break + + val contentLength = closeTagIndex - contentStart + result.add(strippedIndex to (strippedIndex + contentLength)) + + strippedIndex += contentLength + currentIndex = closeTagIndex + 4 + } + + return result +} + +@Composable +private fun SyncScrolling( + originalListState: LazyListState, + revisedListState: LazyListState, + coroutineScope: CoroutineScope +) { + fun syncScroll(leading: LazyListState, following: LazyListState) { + coroutineScope.launch { + val scrollPosition = leading.firstVisibleItemScrollOffset + following.scrollToItem( + index = leading.firstVisibleItemIndex, + scrollOffset = scrollPosition + ) + } + } + + // Observe scroll changes in the LEFT column + LaunchedEffect(originalListState.firstVisibleItemIndex, originalListState.firstVisibleItemScrollOffset) { + syncScroll(originalListState, revisedListState) + } + + // Observe scroll changes in the RIGHT column + LaunchedEffect(revisedListState.firstVisibleItemIndex, revisedListState.firstVisibleItemScrollOffset) { + syncScroll(revisedListState, originalListState) + } +} +// TODO: Evtl. JsonDiffElement data class erstellen welches das JsonTreeElement und den ChangeType hat. Dann beiden JsonTreeElement Listen darauf mappen um die Infos zusammen zu haben \ No newline at end of file diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt new file mode 100644 index 0000000..7a9e3e9 --- /dev/null +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt @@ -0,0 +1,196 @@ +package com.sebastianneubauer.jsontree + +import io.github.petertrr.diffutils.text.DiffRow +import io.github.petertrr.diffutils.text.DiffRowGenerator +import kotlinx.coroutines.Dispatchers + +internal class JsonTreeDiffer2 { + + val original = """ + { + "topLevelObject": { + "string": "stringValue", + "nestedObject": { + "int": 42, + "nestedArray": [ + "nestedArrayValue", + "nestedArrayValue" + ], + "arrayOfObjects": [ + { + "anotherString": "anotherStringValue" + }, + { + "anotherInt": 52 + } + ] + } + }, + "topLevelArray": [ + "hello", + "world" + ], + "emptyObject": { + + } + } + """.trimIndent() + + val revised = """ + { + "topLevelObject": { + "string": "stringValue", + "nestedObject": { + "nestedArray": [ + "nestedArrayValue", + "nestedArrayValue" + ], + "rrayOfa": [ + { + "anotherString": "anotherStringValue" + }, + { + "anotherInt": 52 + }, + { + "anotherFloat": 5.0 + } + ] + } + }, + "topLevelArray": [ + "hello", + "world" + ], + "emptyObject": { + + } + } + """.trimIndent() + + suspend fun diff( + original: String, + revised: String, + ) { + val diffRows = DiffRowGenerator( +// showInlineDiffs = true, +// newTag = object : DiffTagGenerator { +// override fun generateClose(tag: DiffRow.Tag): String { +// return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" +// } +// +// override fun generateOpen(tag: DiffRow.Tag): String { +// return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" +// } +// }, +// oldTag = object : DiffTagGenerator { +// override fun generateClose(tag: DiffRow.Tag): String { +// return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" +// } +// +// override fun generateOpen(tag: DiffRow.Tag): String { +// return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" +// } +// }, + ).generateDiffRows(original.lines(), revised.lines()) + + println(diffRows) + + val originalDiffJson = diffRows.fold("") { acc, row -> acc + row.oldLine } + val revisedDiffJson = diffRows.fold("") { acc, row -> acc + row.newLine } + +// val originalJsonElement = Json.parseToJsonElement(originalDiffJson) +// val revisedJsonElement = Json.parseToJsonElement(revisedDiffJson) + + val originalParser = JsonTreeParser( + json = originalDiffJson, + defaultDispatcher = Dispatchers.Default, + mainDispatcher = Dispatchers.Main + ).also { it.init(TreeState.EXPANDED) } + + val revisedParser = JsonTreeParser( + json = revisedDiffJson, + defaultDispatcher = Dispatchers.Default, + mainDispatcher = Dispatchers.Main + ).also { it.init(TreeState.EXPANDED) } + + //todo actually subscribe to the state + val originalJsonTreeList = (originalParser.state.value as JsonTreeParserState.Ready).list + val revisedJsonTreeList = (revisedParser.state.value as JsonTreeParserState.Ready).list + + println(originalJsonTreeList) + println(revisedJsonTreeList) + val originalUsedIds = mutableListOf() + val originalDiffElements = originalJsonTreeList.map { jsonTreeElement -> + println(jsonTreeElement.toDiffString()) + val row = diffRows.first { diffRow -> + diffRow.oldLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in originalUsedIds + } + originalUsedIds.add(jsonTreeElement.id) + JsonDiffElement( + jsonTreeElement = jsonTreeElement, + changeType = when(row.tag) { + DiffRow.Tag.EQUAL -> ChangeType.Equal + DiffRow.Tag.CHANGE -> ChangeType.Change + DiffRow.Tag.INSERT -> ChangeType.Insertion + DiffRow.Tag.DELETE -> ChangeType.Deletion + } + ) + } + + val revisedUsedIds = mutableListOf() + val revisedDiffElements = revisedJsonTreeList.map { jsonTreeElement -> + println(jsonTreeElement.toDiffString()) + val row = diffRows.first { diffRow -> + diffRow.newLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in revisedUsedIds + } + revisedUsedIds.add(jsonTreeElement.id) + JsonDiffElement( + jsonTreeElement = jsonTreeElement, + changeType = when(row.tag) { + DiffRow.Tag.EQUAL -> ChangeType.Equal + DiffRow.Tag.CHANGE -> ChangeType.Change + DiffRow.Tag.INSERT -> ChangeType.Insertion + DiffRow.Tag.DELETE -> ChangeType.Deletion + } + ) + } + // TODO: überlegen wie man inline diffs unterstützen kann + } + + private fun JsonTreeElement.toDiffString(): String { + return when(this) { + is JsonTreeElement.Collapsable.Object -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { + "\"$key\": {" + } else { + "{" + } + is JsonTreeElement.Collapsable.Array -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { + "\"$key\": [" + } else { + "[" + } + is JsonTreeElement.Primitive -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { + "\"$key\": $value" + if(isLastItem) "" else "," + } else { + "$value" + if(isLastItem) "" else "," + } + is JsonTreeElement.EndBracket -> when(type) { + JsonTreeElement.EndBracket.Type.ARRAY -> "]" + JsonTreeElement.EndBracket.Type.OBJECT -> "}" + } + } + } + + internal data class JsonDiffElement( + val jsonTreeElement: JsonTreeElement, + val changeType: ChangeType + ) + + internal enum class ChangeType { + Change, + Insertion, + Deletion, + Equal + } +} \ No newline at end of file diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt index 33de277..237a30e 100644 --- a/sample/src/commonMain/kotlin/App.kt +++ b/sample/src/commonMain/kotlin/App.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sebastianneubauer.jsontree.JsonTree +import com.sebastianneubauer.jsontree.SideBySideDiff import com.sebastianneubauer.jsontree.TreeColors import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.defaultDarkColors @@ -243,6 +244,10 @@ private fun MainScreen() { Spacer(Modifier.height(8.dp)) + SideBySideDiff() + + Spacer(Modifier.height(8.dp)) + val pagerState = rememberPagerState(initialPage = 0, pageCount = { 3 }) //Pager to test leaving composition From 9362e1fbd8b7023b3e85af69712b6f768d41bf33 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sun, 8 Feb 2026 15:01:37 +0100 Subject: [PATCH 02/32] Fix logic --- .../sebastianneubauer/jsontree/JsonTree.kt | 5 - .../jsontree/JsonTreeDiff2.kt | 293 +++++++++++++++ .../jsontree/JsonTreeDiffer2.kt | 343 ++++++++++-------- 3 files changed, 489 insertions(+), 152 deletions(-) create mode 100644 jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff2.kt diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt index dcb6ff9..657b151 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt @@ -99,11 +99,6 @@ public fun JsonTree( jsonParser.init(initialState) } - LaunchedEffect(Unit) { - val differ = JsonTreeDiffer2() - differ.diff(original = differ.original, revised = differ.revised) - } - when (val state = jsonParser.state.value) { is JsonTreeParserState.Ready -> { Box(modifier = modifier) { diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff2.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff2.kt new file mode 100644 index 0000000..1901ace --- /dev/null +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff2.kt @@ -0,0 +1,293 @@ +package com.sebastianneubauer.jsontree + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import be.digitalia.compose.htmlconverter.htmlToAnnotatedString +import io.github.petertrr.diffutils.text.DiffLineNormalizer +import io.github.petertrr.diffutils.text.DiffRow +import io.github.petertrr.diffutils.text.DiffRowGenerator +import io.github.petertrr.diffutils.text.DiffTagGenerator +import jsontree.jsontree.generated.resources.Res +import jsontree.jsontree.generated.resources.jsontree_arrow_right +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable.start +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.vectorResource + +@Composable +public fun JsonTreeDiff2( + originalJson: String, + revisedJson: String, + onLoading: @Composable () -> Unit, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), + colors: TreeColors = defaultLightColors, + icon: ImageVector = vectorResource(Res.drawable.jsontree_arrow_right), + iconSize: Dp = 20.dp, + textStyle: TextStyle = LocalTextStyle.current, + showIndices: Boolean = false, + showItemCount: Boolean = true, + lazyListState: LazyListState = rememberLazyListState(), + onError: (Throwable) -> Unit = {} +) { + + val jsonTreeDiffer2 = remember { JsonTreeDiffer2() } + LaunchedEffect(originalJson, revisedJson) { + jsonTreeDiffer2.diff(originalJson, revisedJson) + } + + + +// when (val state = revisedJsonParser.state.value) { +// is JsonTreeParserState.Ready -> { +// Box(modifier = modifier) { +// JsonTreeList( +// state = state, +// contentPadding = contentPadding, +// colors = colors, +// icon = icon, +// iconSize = iconSize, +// textStyle = textStyle, +// showIndices = showIndices, +// showItemCount = showItemCount, +// searchResult = SearchState.SearchResult( +// query = null, +// occurrences = emptyMap(), +// selectedOccurrence = null, +// totalResults = 0, +// selectedResultIndex = null +// ), +// lazyListState = lazyListState, +// onClick = {} //noop +// ) +// +// } +// } +// is JsonTreeParserState.Loading -> onLoading() +// is JsonTreeParserState.Parsing.Error -> onError(state.throwable) +// is JsonTreeParserState.Parsing.Parsed -> error("Unexpected state $state") +// } +} + +@Composable +public fun SideBySideDiff2() { + val original = """ + { + "topLevelObject": { + "string": "stringValue", + "nestedObject": { + "int": 42, + "nestedArray": [ + "nestedArrayValue", + "nestedArrayValue" + ], + "arrayOfObjects": [ + { + "anotherString": "anotherStringValue" + }, + { + "anotherInt": 52 + } + ] + } + }, + "topLevelArray": [ + "hello", + "world" + ], + "emptyObject": { + + } + } + """.trimIndent() + + val revised = """ + { + "topLevelObject": { + "string": "stringValue", + "nestedObject": { + "nestedArray": [ + "nestedArrayValue", + "nestedArrayValue" + ], + "rrayOfa": [ + { + "anotherString": "anotherStringValue" + }, + { + "anotherInt": 52 + }, + { + "anotherFloat": 5.0 + } + ] + } + }, + "topLevelArray": [ + "hello", + "world" + ], + "emptyObject": { + + } + } + """.trimIndent() +// TODO: Leerzeilen wie im emptyJsonObject sind schwer zu verarbeiten. Es ist auch nicht garantiert, dass der User pretty Json reingibt. + // TODO: Evtl. die Strings als erstes durch den JsonParser jagen und dann toDiffString verwenden um die DiffRows zu erstellen? + // TODO: Dann hat man garantiert valides Json und kann die DiffRows perfekt mit den JsonTreeElements matchen + + val originalListState = rememberLazyListState() + val revisedListState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() + + val jsonTreeDiffer2 = remember { JsonTreeDiffer2() } + val jsonTreeDiffer2State = jsonTreeDiffer2.state.collectAsState().value + LaunchedEffect(Unit) { + jsonTreeDiffer2.diff(original, revised) + } + + SyncScrolling( + originalListState = originalListState, + revisedListState = revisedListState, + coroutineScope = coroutineScope + ) + + println("State: $jsonTreeDiffer2State") + if(jsonTreeDiffer2State is JsonTreeDiffer2State.Ready) { + Row(modifier = Modifier.fillMaxWidth()) { + LazyColumn( + modifier = Modifier.weight(1f), + state = originalListState + ) { + items(jsonTreeDiffer2State.originalJsonDiffElements) { diffElement -> + val text = when(diffElement) { + is JsonTreeDiffer2.JsonDiffElement.Change -> diffElement.jsonTreeElement.toDiffString() + is JsonTreeDiffer2.JsonDiffElement.Deletion -> diffElement.jsonTreeElement!!.toDiffString() + is JsonTreeDiffer2.JsonDiffElement.Equal -> diffElement.jsonTreeElement.toDiffString() + is JsonTreeDiffer2.JsonDiffElement.Insertion -> "" + } + val annotatedText = buildAnnotatedString { + append(text) + if(diffElement is JsonTreeDiffer2.JsonDiffElement.Change) { + diffElement.inlineDiffIndices.forEach { (start, end) -> + addStyle( + style = SpanStyle(background = Color.Blue), + start = start, + end = end + ) + } + } + } + + Text( + modifier = Modifier + .fillMaxWidth() + .background( + color = when(diffElement) { + is JsonTreeDiffer2.JsonDiffElement.Change -> Color.Red + is JsonTreeDiffer2.JsonDiffElement.Deletion -> Color.Red + is JsonTreeDiffer2.JsonDiffElement.Equal -> Color.Transparent + is JsonTreeDiffer2.JsonDiffElement.Insertion -> Color.Transparent + } + ), + text = annotatedText + ) + } + } + + LazyColumn( + modifier = Modifier.weight(1f), + state = revisedListState + ) { + items(jsonTreeDiffer2State.revisedJsonDiffElements) { diffElement -> + val text = when(diffElement) { + is JsonTreeDiffer2.JsonDiffElement.Change -> diffElement.jsonTreeElement.toDiffString() + is JsonTreeDiffer2.JsonDiffElement.Deletion -> "" + is JsonTreeDiffer2.JsonDiffElement.Equal -> diffElement.jsonTreeElement.toDiffString() + is JsonTreeDiffer2.JsonDiffElement.Insertion -> diffElement.jsonTreeElement!!.toDiffString() + } + val annotatedText = buildAnnotatedString { + append(text) + if(diffElement is JsonTreeDiffer2.JsonDiffElement.Change) { + diffElement.inlineDiffIndices.forEach { (start, end) -> + addStyle( + style = SpanStyle(background = Color.Blue), + start = start, + end = end + ) + } + } + } + + Text( + modifier = Modifier + .fillMaxWidth() + .background( + color = when(diffElement) { + is JsonTreeDiffer2.JsonDiffElement.Change -> Color.Green + is JsonTreeDiffer2.JsonDiffElement.Deletion -> Color.Transparent + is JsonTreeDiffer2.JsonDiffElement.Equal -> Color.Transparent + is JsonTreeDiffer2.JsonDiffElement.Insertion -> Color.Green + } + ), + text = annotatedText + ) + } + } + } + } + +} + +@Composable +private fun SyncScrolling( + originalListState: LazyListState, + revisedListState: LazyListState, + coroutineScope: CoroutineScope +) { + fun syncScroll(leading: LazyListState, following: LazyListState) { + coroutineScope.launch { + val scrollPosition = leading.firstVisibleItemScrollOffset + following.scrollToItem( + index = leading.firstVisibleItemIndex, + scrollOffset = scrollPosition + ) + } + } + + // Observe scroll changes in the LEFT column + LaunchedEffect(originalListState.firstVisibleItemIndex, originalListState.firstVisibleItemScrollOffset) { + syncScroll(originalListState, revisedListState) + } + + // Observe scroll changes in the RIGHT column + LaunchedEffect(revisedListState.firstVisibleItemIndex, revisedListState.firstVisibleItemScrollOffset) { + syncScroll(revisedListState, originalListState) + } +} diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt index 7a9e3e9..5a51d92 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt @@ -2,114 +2,27 @@ package com.sebastianneubauer.jsontree import io.github.petertrr.diffutils.text.DiffRow import io.github.petertrr.diffutils.text.DiffRowGenerator +import io.github.petertrr.diffutils.text.DiffTagGenerator import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.serialization.json.Json internal class JsonTreeDiffer2 { - val original = """ - { - "topLevelObject": { - "string": "stringValue", - "nestedObject": { - "int": 42, - "nestedArray": [ - "nestedArrayValue", - "nestedArrayValue" - ], - "arrayOfObjects": [ - { - "anotherString": "anotherStringValue" - }, - { - "anotherInt": 52 - } - ] - } - }, - "topLevelArray": [ - "hello", - "world" - ], - "emptyObject": { - - } - } - """.trimIndent() - - val revised = """ - { - "topLevelObject": { - "string": "stringValue", - "nestedObject": { - "nestedArray": [ - "nestedArrayValue", - "nestedArrayValue" - ], - "rrayOfa": [ - { - "anotherString": "anotherStringValue" - }, - { - "anotherInt": 52 - }, - { - "anotherFloat": 5.0 - } - ] - } - }, - "topLevelArray": [ - "hello", - "world" - ], - "emptyObject": { - - } - } - """.trimIndent() + val state = MutableStateFlow(JsonTreeDiffer2State.Loading) suspend fun diff( original: String, revised: String, ) { - val diffRows = DiffRowGenerator( -// showInlineDiffs = true, -// newTag = object : DiffTagGenerator { -// override fun generateClose(tag: DiffRow.Tag): String { -// return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" -// } -// -// override fun generateOpen(tag: DiffRow.Tag): String { -// return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" -// } -// }, -// oldTag = object : DiffTagGenerator { -// override fun generateClose(tag: DiffRow.Tag): String { -// return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" -// } -// -// override fun generateOpen(tag: DiffRow.Tag): String { -// return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" -// } -// }, - ).generateDiffRows(original.lines(), revised.lines()) - - println(diffRows) - - val originalDiffJson = diffRows.fold("") { acc, row -> acc + row.oldLine } - val revisedDiffJson = diffRows.fold("") { acc, row -> acc + row.newLine } - -// val originalJsonElement = Json.parseToJsonElement(originalDiffJson) -// val revisedJsonElement = Json.parseToJsonElement(revisedDiffJson) - val originalParser = JsonTreeParser( - json = originalDiffJson, + json = original, defaultDispatcher = Dispatchers.Default, mainDispatcher = Dispatchers.Main ).also { it.init(TreeState.EXPANDED) } val revisedParser = JsonTreeParser( - json = revisedDiffJson, + json = revised, defaultDispatcher = Dispatchers.Default, mainDispatcher = Dispatchers.Main ).also { it.init(TreeState.EXPANDED) } @@ -118,74 +31,178 @@ internal class JsonTreeDiffer2 { val originalJsonTreeList = (originalParser.state.value as JsonTreeParserState.Ready).list val revisedJsonTreeList = (revisedParser.state.value as JsonTreeParserState.Ready).list + val diffRowGenerator = object : DiffTagGenerator { + override fun generateClose(tag: DiffRow.Tag): String { + return inlineDiffTagClosed + } + + override fun generateOpen(tag: DiffRow.Tag): String { + return inlineDiffTagOpen + } + } + + val diffRows = DiffRowGenerator( + showInlineDiffs = true, + newTag = diffRowGenerator, + oldTag = diffRowGenerator, + ).generateDiffRows( + originalJsonTreeList.map { it.toDiffString() }, + revisedJsonTreeList.map { it.toDiffString() } + ) + + println(diffRows) + + val inlineDiffs = diffRows.map { diffRow -> + if(diffRow.tag == DiffRow.Tag.CHANGE) { + val oldLineIndices = diffRow.oldLine.trim().findBoldTagIndicesStripped() + val newLineIndices = diffRow.newLine.trim().findBoldTagIndicesStripped() + println("OldLine: ${diffRow.oldLine}, indices: $oldLineIndices, NewLine: ${diffRow.newLine}, indices: $newLineIndices") + Pair(oldLineIndices, newLineIndices) + } else { + Pair(emptyList(), emptyList()) + } + } + + println("InlineDiffs: $inlineDiffs") + + val strippedDiffRows = diffRows.map { diffRow -> + diffRow.copy( + oldLine = diffRow.oldLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, ""), + newLine = diffRow.newLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, "") + ) + } + + println("strippedDiffs: $strippedDiffRows") + +// val originalDiffJson = strippedDiffRows.fold("") { acc, row -> acc + row.oldLine } +// val revisedDiffJson = strippedDiffRows.fold("") { acc, row -> acc + row.newLine } + println(originalJsonTreeList) println(revisedJsonTreeList) val originalUsedIds = mutableListOf() - val originalDiffElements = originalJsonTreeList.map { jsonTreeElement -> - println(jsonTreeElement.toDiffString()) - val row = diffRows.first { diffRow -> - diffRow.oldLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in originalUsedIds - } - originalUsedIds.add(jsonTreeElement.id) - JsonDiffElement( - jsonTreeElement = jsonTreeElement, - changeType = when(row.tag) { - DiffRow.Tag.EQUAL -> ChangeType.Equal - DiffRow.Tag.CHANGE -> ChangeType.Change - DiffRow.Tag.INSERT -> ChangeType.Insertion - DiffRow.Tag.DELETE -> ChangeType.Deletion + val originalDiffElements = strippedDiffRows.mapIndexed { index, diffRow -> + when(diffRow.tag) { + DiffRow.Tag.EQUAL -> { + val jsonTreeElement = originalJsonTreeList.first { jsonTreeElement -> + println("$diffRow -> ${jsonTreeElement.toDiffString()}") + diffRow.oldLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in originalUsedIds + } + originalUsedIds.add(jsonTreeElement.id) + JsonDiffElement.Equal(jsonTreeElement) } - ) + DiffRow.Tag.CHANGE -> { + val jsonTreeElement = originalJsonTreeList.first { jsonTreeElement -> + println(diffRow) + diffRow.oldLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in originalUsedIds + } + originalUsedIds.add(jsonTreeElement.id) + JsonDiffElement.Change( + jsonTreeElement = jsonTreeElement, + inlineDiffIndices = inlineDiffs[index].first + ) + } + DiffRow.Tag.INSERT -> { + JsonDiffElement.Insertion(null) + } + DiffRow.Tag.DELETE -> { + val jsonTreeElement = originalJsonTreeList.first { jsonTreeElement -> + println(diffRow) + diffRow.oldLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in originalUsedIds + } + originalUsedIds.add(jsonTreeElement.id) + JsonDiffElement.Deletion(jsonTreeElement) + } + } } val revisedUsedIds = mutableListOf() - val revisedDiffElements = revisedJsonTreeList.map { jsonTreeElement -> - println(jsonTreeElement.toDiffString()) - val row = diffRows.first { diffRow -> - diffRow.newLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in revisedUsedIds - } - revisedUsedIds.add(jsonTreeElement.id) - JsonDiffElement( - jsonTreeElement = jsonTreeElement, - changeType = when(row.tag) { - DiffRow.Tag.EQUAL -> ChangeType.Equal - DiffRow.Tag.CHANGE -> ChangeType.Change - DiffRow.Tag.INSERT -> ChangeType.Insertion - DiffRow.Tag.DELETE -> ChangeType.Deletion + val revisedDiffElements = strippedDiffRows.mapIndexed { index, diffRow -> + when(diffRow.tag) { + DiffRow.Tag.EQUAL -> { + val jsonTreeElement = revisedJsonTreeList.first { jsonTreeElement -> + diffRow.newLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in revisedUsedIds + } + revisedUsedIds.add(jsonTreeElement.id) + JsonDiffElement.Equal(jsonTreeElement) } - ) + DiffRow.Tag.CHANGE -> { + val jsonTreeElement = revisedJsonTreeList.first { jsonTreeElement -> + diffRow.newLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in revisedUsedIds + } + revisedUsedIds.add(jsonTreeElement.id) + JsonDiffElement.Change( + jsonTreeElement = jsonTreeElement, + inlineDiffIndices = inlineDiffs[index].second + ) + } + DiffRow.Tag.INSERT -> { + val jsonTreeElement = revisedJsonTreeList.first { jsonTreeElement -> + diffRow.newLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in revisedUsedIds + } + revisedUsedIds.add(jsonTreeElement.id) + JsonDiffElement.Insertion(jsonTreeElement) + } + DiffRow.Tag.DELETE -> JsonDiffElement.Deletion(null) + } } - // TODO: überlegen wie man inline diffs unterstützen kann + + println("OriginalDiffElements: $originalDiffElements") + println("RevisedDiffElements: $revisedDiffElements") + + state.value = JsonTreeDiffer2State.Ready( + originalJsonDiffElements = originalDiffElements, + revisedJsonDiffElements = revisedDiffElements, + ) } - private fun JsonTreeElement.toDiffString(): String { - return when(this) { - is JsonTreeElement.Collapsable.Object -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { - "\"$key\": {" - } else { - "{" - } - is JsonTreeElement.Collapsable.Array -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { - "\"$key\": [" - } else { - "[" - } - is JsonTreeElement.Primitive -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { - "\"$key\": $value" + if(isLastItem) "" else "," - } else { - "$value" + if(isLastItem) "" else "," - } - is JsonTreeElement.EndBracket -> when(type) { - JsonTreeElement.EndBracket.Type.ARRAY -> "]" - JsonTreeElement.EndBracket.Type.OBJECT -> "}" - } + private fun String.findBoldTagIndicesStripped(): List> { + val result = mutableListOf>() + var strippedIndex = 0 + var currentIndex = 0 + + while (currentIndex < length) { + val openTagIndex = indexOf(inlineDiffTagOpen, currentIndex) + if (openTagIndex == -1) break + + // Add non-bold text length to stripped index + strippedIndex += (openTagIndex - currentIndex) + + val contentStart = openTagIndex + inlineDiffTagOpen.length + val closeTagIndex = indexOf(inlineDiffTagClosed, contentStart) + + if (closeTagIndex == -1) break + + val contentLength = closeTagIndex - contentStart + result.add(strippedIndex to (strippedIndex + contentLength)) + + strippedIndex += contentLength + currentIndex = closeTagIndex + inlineDiffTagClosed.length } + + return result } - internal data class JsonDiffElement( - val jsonTreeElement: JsonTreeElement, - val changeType: ChangeType - ) + internal val inlineDiffTagOpen = "<$$$$>" + internal val inlineDiffTagClosed = "" + + internal sealed interface JsonDiffElement{ + data class Change( + val jsonTreeElement: JsonTreeElement, + val inlineDiffIndices: List> + ): JsonDiffElement + + data class Insertion( + val jsonTreeElement: JsonTreeElement?, + ): JsonDiffElement + + data class Deletion( + val jsonTreeElement: JsonTreeElement?, + ): JsonDiffElement + + data class Equal( + val jsonTreeElement: JsonTreeElement, + ): JsonDiffElement + } internal enum class ChangeType { Change, @@ -193,4 +210,36 @@ internal class JsonTreeDiffer2 { Deletion, Equal } +} + +internal sealed interface JsonTreeDiffer2State { + data object Loading: JsonTreeDiffer2State + data class Ready( + val originalJsonDiffElements: List, + val revisedJsonDiffElements: List + ): JsonTreeDiffer2State +} + +internal fun JsonTreeElement.toDiffString(): String { + return when(this) { + is JsonTreeElement.Collapsable.Object -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { + "\"$key\": {" + } else { + "{" + } + is JsonTreeElement.Collapsable.Array -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { + "\"$key\": [" + } else { + "[" + } + is JsonTreeElement.Primitive -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { + "\"$key\": $value" + if(isLastItem) "" else "," + } else { + "$value" + if(isLastItem) "" else "," + } + is JsonTreeElement.EndBracket -> when(type) { + JsonTreeElement.EndBracket.Type.ARRAY -> if (!isLastItem) "]," else "]" + JsonTreeElement.EndBracket.Type.OBJECT -> if (!isLastItem) "}," else "}" + } + } } \ No newline at end of file From 6bc490827ec104a11d5310eb58aedc5b68938000 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sun, 8 Feb 2026 16:23:52 +0100 Subject: [PATCH 03/32] Move code to functions --- .gitignore | 3 + .../jsontree/JsonTreeDiff2.kt | 73 ++----- .../jsontree/JsonTreeDiffer2.kt | 201 ++++++++++-------- sample/src/commonMain/kotlin/App.kt | 3 +- 4 files changed, 133 insertions(+), 147 deletions(-) diff --git a/.gitignore b/.gitignore index 5c1e9c7..8e1bbcc 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,9 @@ captures/ .idea/runConfigurations.xml .idea/AndroidProjectSystem.xml .idea/deviceManager.xml +.idea/appInsightsSettings.xml +.idea/ktlint-plugin.xml +.idea/markdown.xml .kotlin diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff2.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff2.kt index 1901ace..b456688 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff2.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff2.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -19,24 +18,14 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import be.digitalia.compose.htmlconverter.htmlToAnnotatedString -import io.github.petertrr.diffutils.text.DiffLineNormalizer -import io.github.petertrr.diffutils.text.DiffRow -import io.github.petertrr.diffutils.text.DiffRowGenerator -import io.github.petertrr.diffutils.text.DiffTagGenerator import jsontree.jsontree.generated.resources.Res import jsontree.jsontree.generated.resources.jsontree_arrow_right -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.NonCancellable.start -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import org.jetbrains.compose.resources.vectorResource @@ -57,42 +46,7 @@ public fun JsonTreeDiff2( onError: (Throwable) -> Unit = {} ) { - val jsonTreeDiffer2 = remember { JsonTreeDiffer2() } - LaunchedEffect(originalJson, revisedJson) { - jsonTreeDiffer2.diff(originalJson, revisedJson) - } - - -// when (val state = revisedJsonParser.state.value) { -// is JsonTreeParserState.Ready -> { -// Box(modifier = modifier) { -// JsonTreeList( -// state = state, -// contentPadding = contentPadding, -// colors = colors, -// icon = icon, -// iconSize = iconSize, -// textStyle = textStyle, -// showIndices = showIndices, -// showItemCount = showItemCount, -// searchResult = SearchState.SearchResult( -// query = null, -// occurrences = emptyMap(), -// selectedOccurrence = null, -// totalResults = 0, -// selectedResultIndex = null -// ), -// lazyListState = lazyListState, -// onClick = {} //noop -// ) -// -// } -// } -// is JsonTreeParserState.Loading -> onLoading() -// is JsonTreeParserState.Parsing.Error -> onError(state.throwable) -// is JsonTreeParserState.Parsing.Parsed -> error("Unexpected state $state") -// } } @Composable @@ -158,27 +112,26 @@ public fun SideBySideDiff2() { } } """.trimIndent() -// TODO: Leerzeilen wie im emptyJsonObject sind schwer zu verarbeiten. Es ist auch nicht garantiert, dass der User pretty Json reingibt. - // TODO: Evtl. die Strings als erstes durch den JsonParser jagen und dann toDiffString verwenden um die DiffRows zu erstellen? - // TODO: Dann hat man garantiert valides Json und kann die DiffRows perfekt mit den JsonTreeElements matchen - - val originalListState = rememberLazyListState() - val revisedListState = rememberLazyListState() - val coroutineScope = rememberCoroutineScope() - val jsonTreeDiffer2 = remember { JsonTreeDiffer2() } + val jsonTreeDiffer2 = remember { + JsonTreeDiffer2( + defaultDispatcher = Dispatchers.Default, + mainDispatcher = Dispatchers.Main + ) + } val jsonTreeDiffer2State = jsonTreeDiffer2.state.collectAsState().value - LaunchedEffect(Unit) { + LaunchedEffect(original, revised) { jsonTreeDiffer2.diff(original, revised) } - SyncScrolling( + val originalListState = rememberLazyListState() + val revisedListState = rememberLazyListState() + + SyncScrollingEffect( originalListState = originalListState, revisedListState = revisedListState, - coroutineScope = coroutineScope ) - println("State: $jsonTreeDiffer2State") if(jsonTreeDiffer2State is JsonTreeDiffer2State.Ready) { Row(modifier = Modifier.fillMaxWidth()) { LazyColumn( @@ -266,11 +219,11 @@ public fun SideBySideDiff2() { } @Composable -private fun SyncScrolling( +private fun SyncScrollingEffect( originalListState: LazyListState, revisedListState: LazyListState, - coroutineScope: CoroutineScope ) { + val coroutineScope = rememberCoroutineScope() fun syncScroll(leading: LazyListState, following: LazyListState) { coroutineScope.launch { val scrollPosition = leading.firstVisibleItemScrollOffset diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt index 5a51d92..c89e2d0 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt @@ -3,44 +3,51 @@ package com.sebastianneubauer.jsontree import io.github.petertrr.diffutils.text.DiffRow import io.github.petertrr.diffutils.text.DiffRowGenerator import io.github.petertrr.diffutils.text.DiffTagGenerator +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.serialization.json.Json +import kotlinx.coroutines.withContext -internal class JsonTreeDiffer2 { +internal class JsonTreeDiffer2( + val defaultDispatcher: CoroutineDispatcher, + val mainDispatcher: CoroutineDispatcher +) { val state = MutableStateFlow(JsonTreeDiffer2State.Loading) suspend fun diff( original: String, revised: String, - ) { - val originalParser = JsonTreeParser( - json = original, - defaultDispatcher = Dispatchers.Default, - mainDispatcher = Dispatchers.Main - ).also { it.init(TreeState.EXPANDED) } - - val revisedParser = JsonTreeParser( - json = revised, - defaultDispatcher = Dispatchers.Default, - mainDispatcher = Dispatchers.Main - ).also { it.init(TreeState.EXPANDED) } - - //todo actually subscribe to the state - val originalJsonTreeList = (originalParser.state.value as JsonTreeParserState.Ready).list - val revisedJsonTreeList = (revisedParser.state.value as JsonTreeParserState.Ready).list - - val diffRowGenerator = object : DiffTagGenerator { - override fun generateClose(tag: DiffRow.Tag): String { - return inlineDiffTagClosed + ) = withContext(defaultDispatcher) { + val originalJsonTreeListDeferred = async { getJsonTreeList(original) } + val revisedJsonTreeListDeferred = async { getJsonTreeList(revised) } + + val originalJsonTreeListResult = originalJsonTreeListDeferred.await() + val revisedJsonTreeListResult = revisedJsonTreeListDeferred.await() + val (originalJsonTreeList, revisedJsonTreeList) = when { + originalJsonTreeListResult is JsonTreeParserState.Parsing.Error -> { + withContext(mainDispatcher) { + state.value = JsonTreeDiffer2State.Error.OriginalJsonError(originalJsonTreeListResult.throwable) + } + return@withContext } - - override fun generateOpen(tag: DiffRow.Tag): String { - return inlineDiffTagOpen + revisedJsonTreeListResult is JsonTreeParserState.Parsing.Error -> { + withContext(mainDispatcher) { + state.value = JsonTreeDiffer2State.Error.RevisedJsonError(revisedJsonTreeListResult.throwable) + } + return@withContext + } + else -> { + (originalJsonTreeListResult as JsonTreeParserState.Ready).list to (revisedJsonTreeListResult as JsonTreeParserState.Ready).list } } + val diffRowGenerator = object : DiffTagGenerator { + override fun generateClose(tag: DiffRow.Tag): String = inlineDiffTagClosed + override fun generateOpen(tag: DiffRow.Tag): String = inlineDiffTagOpen + } + val diffRows = DiffRowGenerator( showInlineDiffs = true, newTag = diffRowGenerator, @@ -50,112 +57,137 @@ internal class JsonTreeDiffer2 { revisedJsonTreeList.map { it.toDiffString() } ) - println(diffRows) - - val inlineDiffs = diffRows.map { diffRow -> + val inlineDiffsIndices = diffRows.map { diffRow -> if(diffRow.tag == DiffRow.Tag.CHANGE) { - val oldLineIndices = diffRow.oldLine.trim().findBoldTagIndicesStripped() - val newLineIndices = diffRow.newLine.trim().findBoldTagIndicesStripped() - println("OldLine: ${diffRow.oldLine}, indices: $oldLineIndices, NewLine: ${diffRow.newLine}, indices: $newLineIndices") + val oldLineIndices = diffRow.oldLine.findInlineDiffTagIndices() + val newLineIndices = diffRow.newLine.findInlineDiffTagIndices() Pair(oldLineIndices, newLineIndices) } else { Pair(emptyList(), emptyList()) } } - println("InlineDiffs: $inlineDiffs") - - val strippedDiffRows = diffRows.map { diffRow -> + val strippedTagDiffRows = diffRows.map { diffRow -> diffRow.copy( oldLine = diffRow.oldLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, ""), newLine = diffRow.newLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, "") ) } - println("strippedDiffs: $strippedDiffRows") + val originalDiffElements = getOriginalDiffElements( + strippedTagDiffRows = strippedTagDiffRows, + originalJsonTreeList = originalJsonTreeList, + inlineDiffsIndices = inlineDiffsIndices + ) -// val originalDiffJson = strippedDiffRows.fold("") { acc, row -> acc + row.oldLine } -// val revisedDiffJson = strippedDiffRows.fold("") { acc, row -> acc + row.newLine } + val revisedDiffElements = getRevisedDiffElements( + strippedTagDiffRows = strippedTagDiffRows, + revisedJsonTreeList = revisedJsonTreeList, + inlineDiffsIndices = inlineDiffsIndices + ) - println(originalJsonTreeList) - println(revisedJsonTreeList) - val originalUsedIds = mutableListOf() - val originalDiffElements = strippedDiffRows.mapIndexed { index, diffRow -> + withContext(mainDispatcher) { + state.value = JsonTreeDiffer2State.Ready( + originalJsonDiffElements = originalDiffElements, + revisedJsonDiffElements = revisedDiffElements, + ) + } + } + + private fun getOriginalDiffElements( + strippedTagDiffRows: List, + originalJsonTreeList: List, + inlineDiffsIndices: List>, List>>> + ): List { + val usedIds = mutableListOf() + + fun findJsonTreeElement(diffLine: String): JsonTreeElement { + return originalJsonTreeList.first { jsonTreeElement -> + diffLine == jsonTreeElement.toDiffString() && jsonTreeElement.id !in usedIds + } + } + + return strippedTagDiffRows.mapIndexed { index, diffRow -> when(diffRow.tag) { DiffRow.Tag.EQUAL -> { - val jsonTreeElement = originalJsonTreeList.first { jsonTreeElement -> - println("$diffRow -> ${jsonTreeElement.toDiffString()}") - diffRow.oldLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in originalUsedIds - } - originalUsedIds.add(jsonTreeElement.id) + val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) + usedIds.add(jsonTreeElement.id) JsonDiffElement.Equal(jsonTreeElement) } DiffRow.Tag.CHANGE -> { - val jsonTreeElement = originalJsonTreeList.first { jsonTreeElement -> - println(diffRow) - diffRow.oldLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in originalUsedIds - } - originalUsedIds.add(jsonTreeElement.id) + val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) + usedIds.add(jsonTreeElement.id) JsonDiffElement.Change( jsonTreeElement = jsonTreeElement, - inlineDiffIndices = inlineDiffs[index].first + inlineDiffIndices = inlineDiffsIndices[index].first ) } DiffRow.Tag.INSERT -> { JsonDiffElement.Insertion(null) } DiffRow.Tag.DELETE -> { - val jsonTreeElement = originalJsonTreeList.first { jsonTreeElement -> - println(diffRow) - diffRow.oldLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in originalUsedIds - } - originalUsedIds.add(jsonTreeElement.id) + val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) + usedIds.add(jsonTreeElement.id) JsonDiffElement.Deletion(jsonTreeElement) } } } + } + + private fun getRevisedDiffElements( + strippedTagDiffRows: List, + revisedJsonTreeList: List, + inlineDiffsIndices: List>, List>>> + ): List { + val usedIds = mutableListOf() + + fun findJsonTreeElement(diffLine: String): JsonTreeElement { + return revisedJsonTreeList.first { jsonTreeElement -> + diffLine == jsonTreeElement.toDiffString() && jsonTreeElement.id !in usedIds + } + } - val revisedUsedIds = mutableListOf() - val revisedDiffElements = strippedDiffRows.mapIndexed { index, diffRow -> + return strippedTagDiffRows.mapIndexed { index, diffRow -> when(diffRow.tag) { DiffRow.Tag.EQUAL -> { - val jsonTreeElement = revisedJsonTreeList.first { jsonTreeElement -> - diffRow.newLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in revisedUsedIds - } - revisedUsedIds.add(jsonTreeElement.id) + val jsonTreeElement = findJsonTreeElement(diffRow.newLine) + usedIds.add(jsonTreeElement.id) JsonDiffElement.Equal(jsonTreeElement) } DiffRow.Tag.CHANGE -> { - val jsonTreeElement = revisedJsonTreeList.first { jsonTreeElement -> - diffRow.newLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in revisedUsedIds - } - revisedUsedIds.add(jsonTreeElement.id) + val jsonTreeElement = findJsonTreeElement(diffRow.newLine) + usedIds.add(jsonTreeElement.id) JsonDiffElement.Change( jsonTreeElement = jsonTreeElement, - inlineDiffIndices = inlineDiffs[index].second + inlineDiffIndices = inlineDiffsIndices[index].second ) } DiffRow.Tag.INSERT -> { - val jsonTreeElement = revisedJsonTreeList.first { jsonTreeElement -> - diffRow.newLine.trim() == jsonTreeElement.toDiffString() && jsonTreeElement.id !in revisedUsedIds - } - revisedUsedIds.add(jsonTreeElement.id) + val jsonTreeElement = findJsonTreeElement(diffRow.newLine) + usedIds.add(jsonTreeElement.id) JsonDiffElement.Insertion(jsonTreeElement) } DiffRow.Tag.DELETE -> JsonDiffElement.Deletion(null) } } + } - println("OriginalDiffElements: $originalDiffElements") - println("RevisedDiffElements: $revisedDiffElements") + private suspend fun getJsonTreeList(json: String): JsonTreeParserState { + val originalParser = JsonTreeParser( + json = json, + defaultDispatcher = Dispatchers.Default, + mainDispatcher = Dispatchers.Main + ).also { it.init(TreeState.EXPANDED) } - state.value = JsonTreeDiffer2State.Ready( - originalJsonDiffElements = originalDiffElements, - revisedJsonDiffElements = revisedDiffElements, - ) + return when(val state = originalParser.state.value) { + is JsonTreeParserState.Ready -> state + is JsonTreeParserState.Parsing.Error -> state + is JsonTreeParserState.Loading, + is JsonTreeParserState.Parsing.Parsed -> error("Impossible state $state!") + } } - private fun String.findBoldTagIndicesStripped(): List> { + private fun String.findInlineDiffTagIndices(): List> { val result = mutableListOf>() var strippedIndex = 0 var currentIndex = 0 @@ -203,13 +235,6 @@ internal class JsonTreeDiffer2 { val jsonTreeElement: JsonTreeElement, ): JsonDiffElement } - - internal enum class ChangeType { - Change, - Insertion, - Deletion, - Equal - } } internal sealed interface JsonTreeDiffer2State { @@ -218,6 +243,10 @@ internal sealed interface JsonTreeDiffer2State { val originalJsonDiffElements: List, val revisedJsonDiffElements: List ): JsonTreeDiffer2State + sealed interface Error: JsonTreeDiffer2State { + data class OriginalJsonError(val throwable: Throwable): Error + data class RevisedJsonError(val throwable: Throwable): Error + } } internal fun JsonTreeElement.toDiffString(): String { diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt index 237a30e..4ec1ebf 100644 --- a/sample/src/commonMain/kotlin/App.kt +++ b/sample/src/commonMain/kotlin/App.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sebastianneubauer.jsontree.JsonTree import com.sebastianneubauer.jsontree.SideBySideDiff +import com.sebastianneubauer.jsontree.SideBySideDiff2 import com.sebastianneubauer.jsontree.TreeColors import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.defaultDarkColors @@ -244,7 +245,7 @@ private fun MainScreen() { Spacer(Modifier.height(8.dp)) - SideBySideDiff() + SideBySideDiff2() Spacer(Modifier.height(8.dp)) From f823d75b4941951d7180c29ecfc7c622de60aa86 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sat, 14 Feb 2026 15:13:09 +0100 Subject: [PATCH 04/32] Move files --- .idea/inspectionProfiles/Project_Default.xml | 8 + gradle/libs.versions.toml | 2 - jsontree/build.gradle.kts | 1 - .../jsontree/JsonTreeDiff.kt | 372 ------------------ .../JsonTreeDiff.kt} | 68 ++-- .../JsonTreeDiffer.kt} | 87 +--- .../jsontree/diff/JsonTreeDifferState.kt | 37 ++ .../jsontree/util/Extensions.kt | 28 ++ sample/src/commonMain/kotlin/App.kt | 4 +- 9 files changed, 125 insertions(+), 482 deletions(-) delete mode 100644 jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff.kt rename jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/{JsonTreeDiff2.kt => diff/JsonTreeDiff.kt} (71%) rename jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/{JsonTreeDiffer2.kt => diff/JsonTreeDiffer.kt} (74%) create mode 100644 jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 44ca2d9..1360274 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,14 @@ "//if(tag == DiffRow.Tag.CHANGE) "" else "" - } - - override fun generateOpen(tag: DiffRow.Tag): String { - return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" - } - }, - oldTag = object : DiffTagGenerator { - override fun generateClose(tag: DiffRow.Tag): String { - return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" - } - - override fun generateOpen(tag: DiffRow.Tag): String { - return ""//if(tag == DiffRow.Tag.CHANGE) "" else "" - } - }, - ).generateDiffRows(original.lines(), revised.lines()) - - println(diffRows) - - val originalListState = rememberLazyListState() - val revisedListState = rememberLazyListState() - val coroutineScope = rememberCoroutineScope() - - SyncScrolling( - originalListState = originalListState, - revisedListState = revisedListState, - coroutineScope = coroutineScope - ) - - Row(modifier = Modifier.fillMaxWidth()) { - LazyColumn( - modifier = Modifier.weight(1f), - state = originalListState - ) { - items(diffRows) { diffRow -> - val text = when(diffRow.tag) { - DiffRow.Tag.INSERT -> "" - DiffRow.Tag.DELETE -> diffRow.oldLine - DiffRow.Tag.CHANGE -> diffRow.oldLine - DiffRow.Tag.EQUAL -> diffRow.oldLine - } - val strippedText = text.replace("", "").replace("", "") - val annotatedText = buildAnnotatedString { - append(strippedText) - if(diffRow.tag == DiffRow.Tag.CHANGE) { - val indices = text.findBoldTagIndicesStripped() - indices.forEach { (start, end) -> - addStyle( - style = SpanStyle(background = Color.Blue), - start = start, - end = end - ) - } - } - } - - Text( - modifier = Modifier - .fillMaxWidth() - .background( - color = when(diffRow.tag) { - DiffRow.Tag.INSERT -> Color.Transparent - DiffRow.Tag.DELETE -> Color.Red - DiffRow.Tag.CHANGE -> Color.Red - DiffRow.Tag.EQUAL -> Color.Transparent - } - ), - text = annotatedText - ) - } - } - - LazyColumn( - modifier = Modifier.weight(1f), - state = revisedListState - ) { - items(diffRows) { diffRow -> - val text = when(diffRow.tag) { - DiffRow.Tag.INSERT -> diffRow.newLine - DiffRow.Tag.DELETE -> "" - DiffRow.Tag.CHANGE -> diffRow.newLine - DiffRow.Tag.EQUAL -> diffRow.newLine - } - val strippedText = text.replace("", "").replace("", "") - val annotatedText = buildAnnotatedString { - append(strippedText) - if(diffRow.tag == DiffRow.Tag.CHANGE) { - val indices = text.findBoldTagIndicesStripped() - indices.forEach { (start, end) -> - addStyle( - style = SpanStyle(background = Color.Blue), - start = start, - end = end - ) - } - } - } - Text( - modifier = Modifier - .fillMaxWidth() - .background( - color = when(diffRow.tag) { - DiffRow.Tag.INSERT -> Color.Green - DiffRow.Tag.DELETE -> Color.Transparent - DiffRow.Tag.CHANGE -> Color.Green - DiffRow.Tag.EQUAL -> Color.Transparent - } - ), - text = annotatedText - ) - } - } - } -} - -internal fun String.findBoldTagIndicesStripped(): List> { - val result = mutableListOf>() - var strippedIndex = 0 - var currentIndex = 0 - - while (currentIndex < length) { - val openTagIndex = indexOf("", currentIndex) - if (openTagIndex == -1) break - - // Add non-bold text length to stripped index - strippedIndex += (openTagIndex - currentIndex) - - val contentStart = openTagIndex + 3 - val closeTagIndex = indexOf("", contentStart) - - if (closeTagIndex == -1) break - - val contentLength = closeTagIndex - contentStart - result.add(strippedIndex to (strippedIndex + contentLength)) - - strippedIndex += contentLength - currentIndex = closeTagIndex + 4 - } - - return result -} - -@Composable -private fun SyncScrolling( - originalListState: LazyListState, - revisedListState: LazyListState, - coroutineScope: CoroutineScope -) { - fun syncScroll(leading: LazyListState, following: LazyListState) { - coroutineScope.launch { - val scrollPosition = leading.firstVisibleItemScrollOffset - following.scrollToItem( - index = leading.firstVisibleItemIndex, - scrollOffset = scrollPosition - ) - } - } - - // Observe scroll changes in the LEFT column - LaunchedEffect(originalListState.firstVisibleItemIndex, originalListState.firstVisibleItemScrollOffset) { - syncScroll(originalListState, revisedListState) - } - - // Observe scroll changes in the RIGHT column - LaunchedEffect(revisedListState.firstVisibleItemIndex, revisedListState.firstVisibleItemScrollOffset) { - syncScroll(revisedListState, originalListState) - } -} -// TODO: Evtl. JsonDiffElement data class erstellen welches das JsonTreeElement und den ChangeType hat. Dann beiden JsonTreeElement Listen darauf mappen um die Infos zusammen zu haben \ No newline at end of file diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff2.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt similarity index 71% rename from jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff2.kt rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index b456688..77c3b01 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiff2.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -1,4 +1,4 @@ -package com.sebastianneubauer.jsontree +package com.sebastianneubauer.jsontree.diff import androidx.compose.foundation.background import androidx.compose.foundation.layout.PaddingValues @@ -17,32 +17,26 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import jsontree.jsontree.generated.resources.Res -import jsontree.jsontree.generated.resources.jsontree_arrow_right +import com.sebastianneubauer.jsontree.TreeColors +import com.sebastianneubauer.jsontree.defaultLightColors +import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement +import com.sebastianneubauer.jsontree.util.toRenderString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.jetbrains.compose.resources.vectorResource @Composable -public fun JsonTreeDiff2( +public fun JsonTreeDiff( originalJson: String, revisedJson: String, onLoading: @Composable () -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), colors: TreeColors = defaultLightColors, - icon: ImageVector = vectorResource(Res.drawable.jsontree_arrow_right), - iconSize: Dp = 20.dp, textStyle: TextStyle = LocalTextStyle.current, - showIndices: Boolean = false, - showItemCount: Boolean = true, - lazyListState: LazyListState = rememberLazyListState(), onError: (Throwable) -> Unit = {} ) { @@ -113,15 +107,15 @@ public fun SideBySideDiff2() { } """.trimIndent() - val jsonTreeDiffer2 = remember { - JsonTreeDiffer2( + val jsonTreeDiffer = remember { + JsonTreeDiffer( defaultDispatcher = Dispatchers.Default, mainDispatcher = Dispatchers.Main ) } - val jsonTreeDiffer2State = jsonTreeDiffer2.state.collectAsState().value + val jsonTreeDifferState = jsonTreeDiffer.state.collectAsState().value LaunchedEffect(original, revised) { - jsonTreeDiffer2.diff(original, revised) + jsonTreeDiffer.diff(original, revised) } val originalListState = rememberLazyListState() @@ -132,22 +126,22 @@ public fun SideBySideDiff2() { revisedListState = revisedListState, ) - if(jsonTreeDiffer2State is JsonTreeDiffer2State.Ready) { + if(jsonTreeDifferState is JsonTreeDifferState.Ready) { Row(modifier = Modifier.fillMaxWidth()) { LazyColumn( modifier = Modifier.weight(1f), state = originalListState ) { - items(jsonTreeDiffer2State.originalJsonDiffElements) { diffElement -> + items(jsonTreeDifferState.originalJsonDiffElements) { diffElement -> val text = when(diffElement) { - is JsonTreeDiffer2.JsonDiffElement.Change -> diffElement.jsonTreeElement.toDiffString() - is JsonTreeDiffer2.JsonDiffElement.Deletion -> diffElement.jsonTreeElement!!.toDiffString() - is JsonTreeDiffer2.JsonDiffElement.Equal -> diffElement.jsonTreeElement.toDiffString() - is JsonTreeDiffer2.JsonDiffElement.Insertion -> "" + is JsonDiffElement.Change -> diffElement.jsonTreeElement.toRenderString() + is JsonDiffElement.Deletion -> diffElement.jsonTreeElement!!.toRenderString() + is JsonDiffElement.Equal -> diffElement.jsonTreeElement.toRenderString() + is JsonDiffElement.Insertion -> "" } val annotatedText = buildAnnotatedString { append(text) - if(diffElement is JsonTreeDiffer2.JsonDiffElement.Change) { + if(diffElement is JsonDiffElement.Change) { diffElement.inlineDiffIndices.forEach { (start, end) -> addStyle( style = SpanStyle(background = Color.Blue), @@ -163,10 +157,10 @@ public fun SideBySideDiff2() { .fillMaxWidth() .background( color = when(diffElement) { - is JsonTreeDiffer2.JsonDiffElement.Change -> Color.Red - is JsonTreeDiffer2.JsonDiffElement.Deletion -> Color.Red - is JsonTreeDiffer2.JsonDiffElement.Equal -> Color.Transparent - is JsonTreeDiffer2.JsonDiffElement.Insertion -> Color.Transparent + is JsonDiffElement.Change -> Color.Red + is JsonDiffElement.Deletion -> Color.Red + is JsonDiffElement.Equal -> Color.Transparent + is JsonDiffElement.Insertion -> Color.Transparent } ), text = annotatedText @@ -178,16 +172,16 @@ public fun SideBySideDiff2() { modifier = Modifier.weight(1f), state = revisedListState ) { - items(jsonTreeDiffer2State.revisedJsonDiffElements) { diffElement -> + items(jsonTreeDifferState.revisedJsonDiffElements) { diffElement -> val text = when(diffElement) { - is JsonTreeDiffer2.JsonDiffElement.Change -> diffElement.jsonTreeElement.toDiffString() - is JsonTreeDiffer2.JsonDiffElement.Deletion -> "" - is JsonTreeDiffer2.JsonDiffElement.Equal -> diffElement.jsonTreeElement.toDiffString() - is JsonTreeDiffer2.JsonDiffElement.Insertion -> diffElement.jsonTreeElement!!.toDiffString() + is JsonDiffElement.Change -> diffElement.jsonTreeElement.toRenderString() + is JsonDiffElement.Deletion -> "" + is JsonDiffElement.Equal -> diffElement.jsonTreeElement.toRenderString() + is JsonDiffElement.Insertion -> diffElement.jsonTreeElement!!.toRenderString() } val annotatedText = buildAnnotatedString { append(text) - if(diffElement is JsonTreeDiffer2.JsonDiffElement.Change) { + if(diffElement is JsonDiffElement.Change) { diffElement.inlineDiffIndices.forEach { (start, end) -> addStyle( style = SpanStyle(background = Color.Blue), @@ -203,10 +197,10 @@ public fun SideBySideDiff2() { .fillMaxWidth() .background( color = when(diffElement) { - is JsonTreeDiffer2.JsonDiffElement.Change -> Color.Green - is JsonTreeDiffer2.JsonDiffElement.Deletion -> Color.Transparent - is JsonTreeDiffer2.JsonDiffElement.Equal -> Color.Transparent - is JsonTreeDiffer2.JsonDiffElement.Insertion -> Color.Green + is JsonDiffElement.Change -> Color.Green + is JsonDiffElement.Deletion -> Color.Transparent + is JsonDiffElement.Equal -> Color.Transparent + is JsonDiffElement.Insertion -> Color.Green } ), text = annotatedText diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt similarity index 74% rename from jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index c89e2d0..a9ab767 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeDiffer2.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -1,5 +1,11 @@ -package com.sebastianneubauer.jsontree - +package com.sebastianneubauer.jsontree.diff + +import com.sebastianneubauer.jsontree.JsonTreeElement +import com.sebastianneubauer.jsontree.JsonTreeParser +import com.sebastianneubauer.jsontree.JsonTreeParserState +import com.sebastianneubauer.jsontree.TreeState +import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement +import com.sebastianneubauer.jsontree.util.toRenderString import io.github.petertrr.diffutils.text.DiffRow import io.github.petertrr.diffutils.text.DiffRowGenerator import io.github.petertrr.diffutils.text.DiffTagGenerator @@ -9,12 +15,12 @@ import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.withContext -internal class JsonTreeDiffer2( +internal class JsonTreeDiffer( val defaultDispatcher: CoroutineDispatcher, val mainDispatcher: CoroutineDispatcher ) { - val state = MutableStateFlow(JsonTreeDiffer2State.Loading) + val state = MutableStateFlow(JsonTreeDifferState.Loading) suspend fun diff( original: String, @@ -25,16 +31,17 @@ internal class JsonTreeDiffer2( val originalJsonTreeListResult = originalJsonTreeListDeferred.await() val revisedJsonTreeListResult = revisedJsonTreeListDeferred.await() + val (originalJsonTreeList, revisedJsonTreeList) = when { originalJsonTreeListResult is JsonTreeParserState.Parsing.Error -> { withContext(mainDispatcher) { - state.value = JsonTreeDiffer2State.Error.OriginalJsonError(originalJsonTreeListResult.throwable) + state.value = JsonTreeDifferState.Error.OriginalJsonError(originalJsonTreeListResult.throwable) } return@withContext } revisedJsonTreeListResult is JsonTreeParserState.Parsing.Error -> { withContext(mainDispatcher) { - state.value = JsonTreeDiffer2State.Error.RevisedJsonError(revisedJsonTreeListResult.throwable) + state.value = JsonTreeDifferState.Error.RevisedJsonError(revisedJsonTreeListResult.throwable) } return@withContext } @@ -53,8 +60,8 @@ internal class JsonTreeDiffer2( newTag = diffRowGenerator, oldTag = diffRowGenerator, ).generateDiffRows( - originalJsonTreeList.map { it.toDiffString() }, - revisedJsonTreeList.map { it.toDiffString() } + originalJsonTreeList.map { it.toRenderString() }, + revisedJsonTreeList.map { it.toRenderString() } ) val inlineDiffsIndices = diffRows.map { diffRow -> @@ -87,7 +94,7 @@ internal class JsonTreeDiffer2( ) withContext(mainDispatcher) { - state.value = JsonTreeDiffer2State.Ready( + state.value = JsonTreeDifferState.Ready( originalJsonDiffElements = originalDiffElements, revisedJsonDiffElements = revisedDiffElements, ) @@ -103,7 +110,7 @@ internal class JsonTreeDiffer2( fun findJsonTreeElement(diffLine: String): JsonTreeElement { return originalJsonTreeList.first { jsonTreeElement -> - diffLine == jsonTreeElement.toDiffString() && jsonTreeElement.id !in usedIds + diffLine == jsonTreeElement.toRenderString() && jsonTreeElement.id !in usedIds } } @@ -143,7 +150,7 @@ internal class JsonTreeDiffer2( fun findJsonTreeElement(diffLine: String): JsonTreeElement { return revisedJsonTreeList.first { jsonTreeElement -> - diffLine == jsonTreeElement.toDiffString() && jsonTreeElement.id !in usedIds + diffLine == jsonTreeElement.toRenderString() && jsonTreeElement.id !in usedIds } } @@ -214,61 +221,7 @@ internal class JsonTreeDiffer2( return result } - internal val inlineDiffTagOpen = "<$$$$>" - internal val inlineDiffTagClosed = "" - - internal sealed interface JsonDiffElement{ - data class Change( - val jsonTreeElement: JsonTreeElement, - val inlineDiffIndices: List> - ): JsonDiffElement - - data class Insertion( - val jsonTreeElement: JsonTreeElement?, - ): JsonDiffElement - - data class Deletion( - val jsonTreeElement: JsonTreeElement?, - ): JsonDiffElement - - data class Equal( - val jsonTreeElement: JsonTreeElement, - ): JsonDiffElement - } + private val inlineDiffTagOpen = "<$$$$>" + private val inlineDiffTagClosed = "" } -internal sealed interface JsonTreeDiffer2State { - data object Loading: JsonTreeDiffer2State - data class Ready( - val originalJsonDiffElements: List, - val revisedJsonDiffElements: List - ): JsonTreeDiffer2State - sealed interface Error: JsonTreeDiffer2State { - data class OriginalJsonError(val throwable: Throwable): Error - data class RevisedJsonError(val throwable: Throwable): Error - } -} - -internal fun JsonTreeElement.toDiffString(): String { - return when(this) { - is JsonTreeElement.Collapsable.Object -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { - "\"$key\": {" - } else { - "{" - } - is JsonTreeElement.Collapsable.Array -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { - "\"$key\": [" - } else { - "[" - } - is JsonTreeElement.Primitive -> if(key != null && parentType != JsonTreeElement.ParentType.ARRAY) { - "\"$key\": $value" + if(isLastItem) "" else "," - } else { - "$value" + if(isLastItem) "" else "," - } - is JsonTreeElement.EndBracket -> when(type) { - JsonTreeElement.EndBracket.Type.ARRAY -> if (!isLastItem) "]," else "]" - JsonTreeElement.EndBracket.Type.OBJECT -> if (!isLastItem) "}," else "}" - } - } -} \ No newline at end of file diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt new file mode 100644 index 0000000..c43f76c --- /dev/null +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt @@ -0,0 +1,37 @@ +package com.sebastianneubauer.jsontree.diff + +import com.sebastianneubauer.jsontree.JsonTreeElement + +internal sealed interface JsonTreeDifferState { + + data object Loading: JsonTreeDifferState + + data class Ready( + val originalJsonDiffElements: List, + val revisedJsonDiffElements: List + ): JsonTreeDifferState + + sealed interface Error: JsonTreeDifferState { + data class OriginalJsonError(val throwable: Throwable): Error + data class RevisedJsonError(val throwable: Throwable): Error + } + + sealed interface JsonDiffElement{ + data class Change( + val jsonTreeElement: JsonTreeElement, + val inlineDiffIndices: List> + ): JsonDiffElement + + data class Insertion( + val jsonTreeElement: JsonTreeElement?, + ): JsonDiffElement + + data class Deletion( + val jsonTreeElement: JsonTreeElement?, + ): JsonDiffElement + + data class Equal( + val jsonTreeElement: JsonTreeElement, + ): JsonDiffElement + } +} diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/Extensions.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/Extensions.kt index c80c0b4..045024e 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/Extensions.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/Extensions.kt @@ -5,6 +5,7 @@ import com.sebastianneubauer.jsontree.JsonTreeElement.Collapsable.Array import com.sebastianneubauer.jsontree.JsonTreeElement.Collapsable.Object import com.sebastianneubauer.jsontree.JsonTreeElement.EndBracket import com.sebastianneubauer.jsontree.JsonTreeElement.Primitive +import com.sebastianneubauer.jsontree.JsonTreeElement.ParentType import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.endBracket @@ -179,3 +180,30 @@ internal fun JsonTreeElement.toList(): List { addToList(this) return list } + +/** + * Converts a JsonTreeElement to its string representation. + */ +internal fun JsonTreeElement.toRenderString(): String { + return when(this) { + is Object -> if(key != null && parentType != ParentType.ARRAY) { + "\"$key\": {" + } else { + "{" + } + is Array -> if(key != null && parentType != ParentType.ARRAY) { + "\"$key\": [" + } else { + "[" + } + is Primitive -> if(key != null && parentType != ParentType.ARRAY) { + "\"$key\": $value" + if(isLastItem) "" else "," + } else { + "$value" + if(isLastItem) "" else "," + } + is EndBracket -> when(type) { + EndBracket.Type.ARRAY -> if (!isLastItem) "]," else "]" + EndBracket.Type.OBJECT -> if (!isLastItem) "}," else "}" + } + } +} \ No newline at end of file diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt index 4ec1ebf..bc84ced 100644 --- a/sample/src/commonMain/kotlin/App.kt +++ b/sample/src/commonMain/kotlin/App.kt @@ -44,8 +44,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sebastianneubauer.jsontree.JsonTree -import com.sebastianneubauer.jsontree.SideBySideDiff -import com.sebastianneubauer.jsontree.SideBySideDiff2 +import com.sebastianneubauer.jsontree.diff.SideBySideDiff2 import com.sebastianneubauer.jsontree.TreeColors import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.defaultDarkColors @@ -56,7 +55,6 @@ import com.sebastianneubauer.jsontreesample.sample.generated.resources.arrow_dow import com.sebastianneubauer.jsontreesample.sample.generated.resources.arrow_up import com.sebastianneubauer.jsontreesample.ui.theme.JsonTreeTheme import kotlinx.coroutines.launch -import org.jetbrains.compose.resources.imageResource import org.jetbrains.compose.resources.painterResource @Composable From 4885357351f600b48f8c3d6b606aadfbd3297066 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sat, 14 Feb 2026 16:52:45 +0100 Subject: [PATCH 05/32] Add syntax highlighting to Diff --- .../jsontree/diff/AnnotatedDiffText.kt | 130 +++++++++++++ .../jsontree/diff/JsonTreeDiff.kt | 181 +++++++++++------- 2 files changed, 246 insertions(+), 65 deletions(-) create mode 100644 jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt new file mode 100644 index 0000000..cbe2a07 --- /dev/null +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt @@ -0,0 +1,130 @@ +package com.sebastianneubauer.jsontree.diff + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import com.sebastianneubauer.jsontree.CollapsableType +import com.sebastianneubauer.jsontree.JsonTreeElement +import com.sebastianneubauer.jsontree.JsonTreeElement.ParentType +import com.sebastianneubauer.jsontree.TreeColors +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.floatOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.longOrNull + +@Composable +internal fun rememberCollapsableDiffText( + type: CollapsableType, + key: String?, + colors: TreeColors, + isLastItem: Boolean, + parentType: ParentType, + diffIndices: List>?, +): AnnotatedString { + return remember(colors) { + buildAnnotatedString { + key?.let { key -> + if (parentType != ParentType.ARRAY) { + withStyle(SpanStyle(color = colors.keyColor)) { + append("\"$key\"") + } + + withStyle(SpanStyle(color = colors.symbolColor)) { + append(": ") + } + } + } + + withStyle(SpanStyle(color = colors.symbolColor)) { + val openBracket = if (type == CollapsableType.OBJECT) "{" else "[" + append(openBracket) + } + + diffIndices?.forEach { (start, end) -> + addStyle( + style = SpanStyle(background = Color.Blue), + start = start, + end = end + ) + } + } + } +} + +@Composable +internal fun rememberPrimitiveDiffText( + key: String?, + value: JsonPrimitive, + colors: TreeColors, + isLastItem: Boolean, + parentType: ParentType, + diffIndices: List>?, +): AnnotatedString { + val valueColor = remember(value) { + when { + value.isString -> colors.stringValueColor + value.booleanOrNull != null -> colors.booleanValueColor + value.doubleOrNull != null || + value.intOrNull != null || + value.floatOrNull != null || + value.longOrNull != null -> colors.numberValueColor + else -> colors.nullValueColor + } + } + + return remember(colors) { + buildAnnotatedString { + key?.let { key -> + if (parentType != ParentType.ARRAY) { + withStyle(SpanStyle(color = colors.keyColor)) { + append("\"$key\"") + } + + withStyle(SpanStyle(color = colors.symbolColor)) { + append(": ") + } + } + } + + withStyle(SpanStyle(color = valueColor)) { + append(value.toString()) + } + + if (!isLastItem) { + withStyle(SpanStyle(color = colors.symbolColor)) { + append(",") + } + } + + diffIndices?.forEach { (start, end) -> + addStyle( + style = SpanStyle(background = Color.Blue), + start = start, + end = end + ) + } + } + } +} + +@Composable +internal fun rememberEndBracketDiffText( + type: JsonTreeElement.EndBracket.Type, + colors: TreeColors, + isLastItem: Boolean, +): AnnotatedString { + return remember(colors) { + buildAnnotatedString { + withStyle(SpanStyle(color = colors.symbolColor)) { + val endBracket = if (type == JsonTreeElement.EndBracket.Type.OBJECT) "}" else "]" + append(if (!isLastItem) "$endBracket," else endBracket) + } + } + } +} \ No newline at end of file diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index 77c3b01..805faf0 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -4,9 +4,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text @@ -17,14 +18,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.sebastianneubauer.jsontree.CollapsableType +import com.sebastianneubauer.jsontree.JsonTreeElement import com.sebastianneubauer.jsontree.TreeColors import com.sebastianneubauer.jsontree.defaultLightColors import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement -import com.sebastianneubauer.jsontree.util.toRenderString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -44,7 +46,9 @@ public fun JsonTreeDiff( } @Composable -public fun SideBySideDiff2() { +public fun SideBySideDiff2( + colors: TreeColors = defaultLightColors, +) { val original = """ { "topLevelObject": { @@ -132,38 +136,35 @@ public fun SideBySideDiff2() { modifier = Modifier.weight(1f), state = originalListState ) { - items(jsonTreeDifferState.originalJsonDiffElements) { diffElement -> - val text = when(diffElement) { - is JsonDiffElement.Change -> diffElement.jsonTreeElement.toRenderString() - is JsonDiffElement.Deletion -> diffElement.jsonTreeElement!!.toRenderString() - is JsonDiffElement.Equal -> diffElement.jsonTreeElement.toRenderString() - is JsonDiffElement.Insertion -> "" - } - val annotatedText = buildAnnotatedString { - append(text) - if(diffElement is JsonDiffElement.Change) { - diffElement.inlineDiffIndices.forEach { (start, end) -> - addStyle( - style = SpanStyle(background = Color.Blue), - start = start, - end = end - ) - } - } + itemsIndexed(jsonTreeDifferState.originalJsonDiffElements) { index, diffElement -> + val jsonTreeElement = when(diffElement) { + is JsonDiffElement.Change -> diffElement.jsonTreeElement + is JsonDiffElement.Deletion -> diffElement.jsonTreeElement!! + is JsonDiffElement.Equal -> diffElement.jsonTreeElement + is JsonDiffElement.Insertion -> null } - Text( - modifier = Modifier - .fillMaxWidth() - .background( - color = when(diffElement) { - is JsonDiffElement.Change -> Color.Red - is JsonDiffElement.Deletion -> Color.Red - is JsonDiffElement.Equal -> Color.Transparent - is JsonDiffElement.Insertion -> Color.Transparent - } - ), - text = annotatedText + val text = rememberText( + jsonTreeElement = jsonTreeElement, + diffIndices = if(diffElement is JsonDiffElement.Change) { + diffElement.inlineDiffIndices + } else null, + colors = colors + ) + + DiffText( + backgroundColor = when(diffElement) { + is JsonDiffElement.Change -> Color.Red + is JsonDiffElement.Deletion -> Color.Red + is JsonDiffElement.Equal -> Color.Transparent + is JsonDiffElement.Insertion -> Color.Transparent + }, + indent = if(jsonTreeElement != null && index > 0) { + 20.dp * jsonTreeElement.level + } else { + 0.dp + }, + text = text, ) } } @@ -172,44 +173,94 @@ public fun SideBySideDiff2() { modifier = Modifier.weight(1f), state = revisedListState ) { - items(jsonTreeDifferState.revisedJsonDiffElements) { diffElement -> - val text = when(diffElement) { - is JsonDiffElement.Change -> diffElement.jsonTreeElement.toRenderString() - is JsonDiffElement.Deletion -> "" - is JsonDiffElement.Equal -> diffElement.jsonTreeElement.toRenderString() - is JsonDiffElement.Insertion -> diffElement.jsonTreeElement!!.toRenderString() - } - val annotatedText = buildAnnotatedString { - append(text) - if(diffElement is JsonDiffElement.Change) { - diffElement.inlineDiffIndices.forEach { (start, end) -> - addStyle( - style = SpanStyle(background = Color.Blue), - start = start, - end = end - ) - } - } + itemsIndexed(jsonTreeDifferState.revisedJsonDiffElements) { index, diffElement -> + val jsonTreeElement = when(diffElement) { + is JsonDiffElement.Change -> diffElement.jsonTreeElement + is JsonDiffElement.Deletion -> null + is JsonDiffElement.Equal -> diffElement.jsonTreeElement + is JsonDiffElement.Insertion -> diffElement.jsonTreeElement!! } + val text = rememberText( + jsonTreeElement = jsonTreeElement, + diffIndices = if(diffElement is JsonDiffElement.Change) { + diffElement.inlineDiffIndices + } else null, + colors = colors + ) - Text( - modifier = Modifier - .fillMaxWidth() - .background( - color = when(diffElement) { - is JsonDiffElement.Change -> Color.Green - is JsonDiffElement.Deletion -> Color.Transparent - is JsonDiffElement.Equal -> Color.Transparent - is JsonDiffElement.Insertion -> Color.Green - } - ), - text = annotatedText + DiffText( + backgroundColor = when(diffElement) { + is JsonDiffElement.Change -> Color.Green + is JsonDiffElement.Deletion -> Color.Transparent + is JsonDiffElement.Equal -> Color.Transparent + is JsonDiffElement.Insertion -> Color.Green + }, + indent = if(jsonTreeElement != null && index > 0) { + 20.dp * jsonTreeElement.level + } else { + 0.dp + }, + text = text ) } } } } +} + +@Composable +private fun DiffText( + backgroundColor: Color, + indent: Dp, + text: AnnotatedString, +) { + Text( + modifier = Modifier + .fillMaxWidth() + .background(color = backgroundColor) + .padding(start = indent), + text = text + ) +} +@Composable +private fun rememberText( + jsonTreeElement: JsonTreeElement?, + diffIndices: List>?, + colors: TreeColors, +): AnnotatedString { + return when(jsonTreeElement) { + is JsonTreeElement.Collapsable.Array -> rememberCollapsableDiffText( + type = CollapsableType.ARRAY, + key = jsonTreeElement.key, + isLastItem = jsonTreeElement.isLastItem, + parentType = jsonTreeElement.parentType, + colors = colors, + diffIndices = diffIndices, + ) + is JsonTreeElement.Collapsable.Object -> rememberCollapsableDiffText( + type = CollapsableType.OBJECT, + key = jsonTreeElement.key, + isLastItem = jsonTreeElement.isLastItem, + parentType = jsonTreeElement.parentType, + colors = colors, + diffIndices = diffIndices, + ) + is JsonTreeElement.Primitive -> rememberPrimitiveDiffText( + key = jsonTreeElement.key, + value = jsonTreeElement.value, + isLastItem = jsonTreeElement.isLastItem, + parentType = jsonTreeElement.parentType, + colors = colors, + diffIndices = diffIndices, + ) + is JsonTreeElement.EndBracket -> rememberEndBracketDiffText( + type = jsonTreeElement.type, + isLastItem = jsonTreeElement.isLastItem, + colors = colors + ) + null -> AnnotatedString("") + } } @Composable From 30418c910f41b137268ff57360cbe0a6c325eef8 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sun, 15 Feb 2026 09:31:55 +0100 Subject: [PATCH 06/32] Add diff colors --- .../jsontree/diff/AnnotatedDiffText.kt | 12 ++-- .../jsontree/diff/JsonTreeDiff.kt | 22 ++++--- .../jsontree/diff/JsonTreeDiffColors.kt | 62 +++++++++++++++++++ 3 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt index cbe2a07..18d1bd8 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt @@ -22,7 +22,8 @@ import kotlinx.serialization.json.longOrNull internal fun rememberCollapsableDiffText( type: CollapsableType, key: String?, - colors: TreeColors, + colors: JsonTreeDiffColors, + highlightColor: Color, isLastItem: Boolean, parentType: ParentType, diffIndices: List>?, @@ -48,7 +49,7 @@ internal fun rememberCollapsableDiffText( diffIndices?.forEach { (start, end) -> addStyle( - style = SpanStyle(background = Color.Blue), + style = SpanStyle(background = highlightColor), start = start, end = end ) @@ -61,7 +62,8 @@ internal fun rememberCollapsableDiffText( internal fun rememberPrimitiveDiffText( key: String?, value: JsonPrimitive, - colors: TreeColors, + colors: JsonTreeDiffColors, + highlightColor: Color, isLastItem: Boolean, parentType: ParentType, diffIndices: List>?, @@ -104,7 +106,7 @@ internal fun rememberPrimitiveDiffText( diffIndices?.forEach { (start, end) -> addStyle( - style = SpanStyle(background = Color.Blue), + style = SpanStyle(background = highlightColor), start = start, end = end ) @@ -116,7 +118,7 @@ internal fun rememberPrimitiveDiffText( @Composable internal fun rememberEndBracketDiffText( type: JsonTreeElement.EndBracket.Type, - colors: TreeColors, + colors: JsonTreeDiffColors, isLastItem: Boolean, ): AnnotatedString { return remember(colors) { diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index 805faf0..87627a7 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -47,7 +47,7 @@ public fun JsonTreeDiff( @Composable public fun SideBySideDiff2( - colors: TreeColors = defaultLightColors, + colors: JsonTreeDiffColors = defaultDarkDiffColors ) { val original = """ { @@ -149,13 +149,14 @@ public fun SideBySideDiff2( diffIndices = if(diffElement is JsonDiffElement.Change) { diffElement.inlineDiffIndices } else null, - colors = colors + colors = colors, + highlightColor = colors.deletionHighlightColor, ) DiffText( backgroundColor = when(diffElement) { - is JsonDiffElement.Change -> Color.Red - is JsonDiffElement.Deletion -> Color.Red + is JsonDiffElement.Change -> colors.deletionBackgroundColor + is JsonDiffElement.Deletion -> colors.deletionBackgroundColor is JsonDiffElement.Equal -> Color.Transparent is JsonDiffElement.Insertion -> Color.Transparent }, @@ -185,15 +186,16 @@ public fun SideBySideDiff2( diffIndices = if(diffElement is JsonDiffElement.Change) { diffElement.inlineDiffIndices } else null, - colors = colors + colors = colors, + highlightColor = colors.insertionHighlightColor ) DiffText( backgroundColor = when(diffElement) { - is JsonDiffElement.Change -> Color.Green + is JsonDiffElement.Change -> colors.insertionBackgroundColor is JsonDiffElement.Deletion -> Color.Transparent is JsonDiffElement.Equal -> Color.Transparent - is JsonDiffElement.Insertion -> Color.Green + is JsonDiffElement.Insertion -> colors.insertionBackgroundColor }, indent = if(jsonTreeElement != null && index > 0) { 20.dp * jsonTreeElement.level @@ -227,7 +229,8 @@ private fun DiffText( private fun rememberText( jsonTreeElement: JsonTreeElement?, diffIndices: List>?, - colors: TreeColors, + colors: JsonTreeDiffColors, + highlightColor: Color, ): AnnotatedString { return when(jsonTreeElement) { is JsonTreeElement.Collapsable.Array -> rememberCollapsableDiffText( @@ -236,6 +239,7 @@ private fun rememberText( isLastItem = jsonTreeElement.isLastItem, parentType = jsonTreeElement.parentType, colors = colors, + highlightColor = highlightColor, diffIndices = diffIndices, ) is JsonTreeElement.Collapsable.Object -> rememberCollapsableDiffText( @@ -244,6 +248,7 @@ private fun rememberText( isLastItem = jsonTreeElement.isLastItem, parentType = jsonTreeElement.parentType, colors = colors, + highlightColor = highlightColor, diffIndices = diffIndices, ) is JsonTreeElement.Primitive -> rememberPrimitiveDiffText( @@ -252,6 +257,7 @@ private fun rememberText( isLastItem = jsonTreeElement.isLastItem, parentType = jsonTreeElement.parentType, colors = colors, + highlightColor = highlightColor, diffIndices = diffIndices, ) is JsonTreeElement.EndBracket -> rememberEndBracketDiffText( diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt new file mode 100644 index 0000000..843e35a --- /dev/null +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt @@ -0,0 +1,62 @@ +package com.sebastianneubauer.jsontree.diff + +import androidx.compose.ui.graphics.Color + +/** + * The color palette for the json diff. + * + * @param keyColor The color for json keys. + * @param stringValueColor The color for json strings. + * @param numberValueColor The color for json numbers. + * @param booleanValueColor The color for json booleans. + * @param nullValueColor The color for json nulls. + * @param symbolColor The color for all symbols like brackets, colons and commas. + * @param deletionHighlightColor The color for highlighting deletions inside a line. + * @param deletionBackgroundColor The background color for lines that contain deletion diffs. + * @param insertionHighlightColor TThe color for highlighting insertions inside a line. + * @param insertionBackgroundColor The background color for lines that contain insertion diffs. + */ +public data class JsonTreeDiffColors( + val keyColor: Color, + val stringValueColor: Color, + val numberValueColor: Color, + val booleanValueColor: Color, + val nullValueColor: Color, + val symbolColor: Color, + val deletionHighlightColor: Color, + val deletionBackgroundColor: Color, + val insertionHighlightColor: Color, + val insertionBackgroundColor: Color, +) + +/** + * The default light palette for the json diff. + */ +public val defaultLightDiffColors: JsonTreeDiffColors = JsonTreeDiffColors( + keyColor = Color(0xFF1F9E8F), + stringValueColor = Color(0xFFE9613F), + numberValueColor = Color(0xFFF7964A), + booleanValueColor = Color(0xFFE9BB4D), + nullValueColor = Color(0xFFE9BB4D), + symbolColor = Color(0xFF1D4555), + deletionHighlightColor = Color(0xFFFECECA), + deletionBackgroundColor = Color(0xFFFFEBE9), + insertionHighlightColor = Color(0xFFACEEBB), + insertionBackgroundColor = Color(0xFFDBFBE1), +) + +/** + * The default dark palette for the json diff. + */ +public val defaultDarkDiffColors: JsonTreeDiffColors = JsonTreeDiffColors( + keyColor = Color(0xFF73c8a9), + stringValueColor = Color(0xFFbd5532), + numberValueColor = Color(0xFFe1b866), + booleanValueColor = Color(0xFFdee1b6), + nullValueColor = Color(0xFFdee1b6), + symbolColor = Color(0xFF798199), + deletionHighlightColor = Color(0xFF7C3C3D), + deletionBackgroundColor = Color(0xFF352D33), + insertionHighlightColor = Color(0xFF345E3D), + insertionBackgroundColor = Color(0xFF263834), +) From 9be16ed9ff5a482b0f94a65fed3b6167c7f802fc Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sun, 15 Feb 2026 10:53:02 +0100 Subject: [PATCH 07/32] Integrate JsonDiff in sample --- .../jsontree/diff/JsonTreeDiff.kt | 317 +-- .../jsontree/diff/JsonTreeDiffError.kt | 14 + .../jsontree/diff/JsonTreeDiffer.kt | 5 +- .../jsontree/diff/JsonTreeDifferState.kt | 4 + sample/src/commonMain/kotlin/App.kt | 68 +- .../commonMain/kotlin/JsonTreeDiffStrings.kt | 2108 +++++++++++++++++ 6 files changed, 2363 insertions(+), 153 deletions(-) create mode 100644 jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffError.kt create mode 100644 sample/src/commonMain/kotlin/JsonTreeDiffStrings.kt diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index 87627a7..0c5c888 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -1,8 +1,9 @@ package com.sebastianneubauer.jsontree.diff import androidx.compose.foundation.background -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -24,9 +25,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.sebastianneubauer.jsontree.CollapsableType import com.sebastianneubauer.jsontree.JsonTreeElement -import com.sebastianneubauer.jsontree.TreeColors -import com.sebastianneubauer.jsontree.defaultLightColors import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement +import com.sebastianneubauer.jsontree.diff.JsonTreeDiffError.OriginalJsonError +import com.sebastianneubauer.jsontree.diff.JsonTreeDiffError.RevisedJsonError import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -36,90 +37,82 @@ public fun JsonTreeDiff( revisedJson: String, onLoading: @Composable () -> Unit, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp), - colors: TreeColors = defaultLightColors, + colors: JsonTreeDiffColors = defaultLightDiffColors, textStyle: TextStyle = LocalTextStyle.current, - onError: (Throwable) -> Unit = {} + onError: (JsonTreeDiffError) -> Unit = {} ) { +// val original = """ +// { +// "topLevelObject": { +// "string": "stringValue", +// "nestedObject": { +// "int": 42, +// "nestedArray": [ +// "nestedArrayValue", +// "nestedArrayValue" +// ], +// "arrayOfObjects": [ +// { +// "anotherString": "anotherStringValue" +// }, +// { +// "anotherInt": 52 +// } +// ] +// } +// }, +// "topLevelArray": [ +// "hello", +// "world" +// ], +// "emptyObject": { +// +// } +// } +// """.trimIndent() +// +// val revised = """ +// { +// "topLevelObject": { +// "string": "stringValue", +// "nestedObject": { +// "nestedArray": [ +// "nestedArrayValue", +// "nestedArrayValue" +// ], +// "rrayOfa": [ +// { +// "anotherString": "anotherStringValue" +// }, +// { +// "anotherInt": 52 +// }, +// { +// "anotherFloat": 5.0 +// } +// ] +// } +// }, +// "topLevelArray": [ +// "hello", +// "world" +// ], +// "emptyObject": { +// +// } +// } +// """.trimIndent() - -} - -@Composable -public fun SideBySideDiff2( - colors: JsonTreeDiffColors = defaultDarkDiffColors -) { - val original = """ - { - "topLevelObject": { - "string": "stringValue", - "nestedObject": { - "int": 42, - "nestedArray": [ - "nestedArrayValue", - "nestedArrayValue" - ], - "arrayOfObjects": [ - { - "anotherString": "anotherStringValue" - }, - { - "anotherInt": 52 - } - ] - } - }, - "topLevelArray": [ - "hello", - "world" - ], - "emptyObject": { - - } - } - """.trimIndent() - - val revised = """ - { - "topLevelObject": { - "string": "stringValue", - "nestedObject": { - "nestedArray": [ - "nestedArrayValue", - "nestedArrayValue" - ], - "rrayOfa": [ - { - "anotherString": "anotherStringValue" - }, - { - "anotherInt": 52 - }, - { - "anotherFloat": 5.0 - } - ] - } - }, - "topLevelArray": [ - "hello", - "world" - ], - "emptyObject": { - - } - } - """.trimIndent() - - val jsonTreeDiffer = remember { + val jsonTreeDiffer = remember(originalJson, revisedJson) { JsonTreeDiffer( defaultDispatcher = Dispatchers.Default, mainDispatcher = Dispatchers.Main ) } - val jsonTreeDifferState = jsonTreeDiffer.state.collectAsState().value - LaunchedEffect(original, revised) { - jsonTreeDiffer.diff(original, revised) + val state = jsonTreeDiffer.state.collectAsState().value + + LaunchedEffect(Unit) { + jsonTreeDiffer.diff(originalJson, revisedJson) } val originalListState = rememberLazyListState() @@ -130,81 +123,105 @@ public fun SideBySideDiff2( revisedListState = revisedListState, ) - if(jsonTreeDifferState is JsonTreeDifferState.Ready) { - Row(modifier = Modifier.fillMaxWidth()) { - LazyColumn( - modifier = Modifier.weight(1f), - state = originalListState - ) { - itemsIndexed(jsonTreeDifferState.originalJsonDiffElements) { index, diffElement -> - val jsonTreeElement = when(diffElement) { - is JsonDiffElement.Change -> diffElement.jsonTreeElement - is JsonDiffElement.Deletion -> diffElement.jsonTreeElement!! - is JsonDiffElement.Equal -> diffElement.jsonTreeElement - is JsonDiffElement.Insertion -> null - } - - val text = rememberText( - jsonTreeElement = jsonTreeElement, - diffIndices = if(diffElement is JsonDiffElement.Change) { - diffElement.inlineDiffIndices - } else null, - colors = colors, - highlightColor = colors.deletionHighlightColor, - ) + when(state) { + is JsonTreeDifferState.Loading -> onLoading() + is JsonTreeDifferState.Ready -> Box(modifier = modifier) { + SideBySideDiff( + state = state, + originalListState = originalListState, + revisedListState = revisedListState, + colors = colors, + textStyle = textStyle, + ) + } + is JsonTreeDifferState.Error.OriginalJsonError -> onError(OriginalJsonError(state.throwable)) + is JsonTreeDifferState.Error.RevisedJsonError -> onError(RevisedJsonError(state.throwable)) + } +} - DiffText( - backgroundColor = when(diffElement) { - is JsonDiffElement.Change -> colors.deletionBackgroundColor - is JsonDiffElement.Deletion -> colors.deletionBackgroundColor - is JsonDiffElement.Equal -> Color.Transparent - is JsonDiffElement.Insertion -> Color.Transparent - }, - indent = if(jsonTreeElement != null && index > 0) { - 20.dp * jsonTreeElement.level - } else { - 0.dp - }, - text = text, - ) +@Composable +private fun SideBySideDiff( + state: JsonTreeDifferState.Ready, + originalListState: LazyListState, + revisedListState: LazyListState, + colors: JsonTreeDiffColors, + textStyle: TextStyle, +) { + Row(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.weight(1f), + state = originalListState + ) { + itemsIndexed(state.originalJsonDiffElements) { index, diffElement -> + val jsonTreeElement = when(diffElement) { + is JsonDiffElement.Change -> diffElement.jsonTreeElement + is JsonDiffElement.Deletion -> diffElement.jsonTreeElement!! + is JsonDiffElement.Equal -> diffElement.jsonTreeElement + is JsonDiffElement.Insertion -> null } - } - LazyColumn( - modifier = Modifier.weight(1f), - state = revisedListState - ) { - itemsIndexed(jsonTreeDifferState.revisedJsonDiffElements) { index, diffElement -> - val jsonTreeElement = when(diffElement) { - is JsonDiffElement.Change -> diffElement.jsonTreeElement - is JsonDiffElement.Deletion -> null - is JsonDiffElement.Equal -> diffElement.jsonTreeElement - is JsonDiffElement.Insertion -> diffElement.jsonTreeElement!! - } - val text = rememberText( - jsonTreeElement = jsonTreeElement, - diffIndices = if(diffElement is JsonDiffElement.Change) { - diffElement.inlineDiffIndices - } else null, - colors = colors, - highlightColor = colors.insertionHighlightColor - ) + val text = rememberText( + jsonTreeElement = jsonTreeElement, + diffIndices = if(diffElement is JsonDiffElement.Change) { + diffElement.inlineDiffIndices + } else null, + colors = colors, + highlightColor = colors.deletionHighlightColor, + ) - DiffText( - backgroundColor = when(diffElement) { - is JsonDiffElement.Change -> colors.insertionBackgroundColor - is JsonDiffElement.Deletion -> Color.Transparent - is JsonDiffElement.Equal -> Color.Transparent - is JsonDiffElement.Insertion -> colors.insertionBackgroundColor - }, - indent = if(jsonTreeElement != null && index > 0) { - 20.dp * jsonTreeElement.level - } else { - 0.dp - }, - text = text - ) + DiffText( + backgroundColor = when(diffElement) { + is JsonDiffElement.Change -> colors.deletionBackgroundColor + is JsonDiffElement.Deletion -> colors.deletionBackgroundColor + is JsonDiffElement.Equal -> Color.Transparent + is JsonDiffElement.Insertion -> Color.Transparent + }, + indent = if(jsonTreeElement != null && index > 0) { + 20.dp * jsonTreeElement.level + } else { + 0.dp + }, + textStyle = textStyle, + text = text, + ) + } + } + + LazyColumn( + modifier = Modifier.weight(1f), + state = revisedListState + ) { + itemsIndexed(state.revisedJsonDiffElements) { index, diffElement -> + val jsonTreeElement = when(diffElement) { + is JsonDiffElement.Change -> diffElement.jsonTreeElement + is JsonDiffElement.Deletion -> null + is JsonDiffElement.Equal -> diffElement.jsonTreeElement + is JsonDiffElement.Insertion -> diffElement.jsonTreeElement!! } + val text = rememberText( + jsonTreeElement = jsonTreeElement, + diffIndices = if(diffElement is JsonDiffElement.Change) { + diffElement.inlineDiffIndices + } else null, + colors = colors, + highlightColor = colors.insertionHighlightColor + ) + + DiffText( + backgroundColor = when(diffElement) { + is JsonDiffElement.Change -> colors.insertionBackgroundColor + is JsonDiffElement.Deletion -> Color.Transparent + is JsonDiffElement.Equal -> Color.Transparent + is JsonDiffElement.Insertion -> colors.insertionBackgroundColor + }, + indent = if(jsonTreeElement != null && index > 0) { + 20.dp * jsonTreeElement.level + } else { + 0.dp + }, + textStyle = textStyle, + text = text + ) } } } @@ -214,6 +231,7 @@ public fun SideBySideDiff2( private fun DiffText( backgroundColor: Color, indent: Dp, + textStyle: TextStyle, text: AnnotatedString, ) { Text( @@ -221,6 +239,7 @@ private fun DiffText( .fillMaxWidth() .background(color = backgroundColor) .padding(start = indent), + style = textStyle, text = text ) } diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffError.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffError.kt new file mode 100644 index 0000000..e984e89 --- /dev/null +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffError.kt @@ -0,0 +1,14 @@ +package com.sebastianneubauer.jsontree.diff + +public interface JsonTreeDiffError { + public val throwable: Throwable + /** + * Describes an error during parsing of the original Json. + */ + public class OriginalJsonError(override val throwable: Throwable): JsonTreeDiffError + + /** + * Describes an error during parsing of the revised Json. + */ + public class RevisedJsonError(override val throwable: Throwable): JsonTreeDiffError +} \ No newline at end of file diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index a9ab767..cba7808 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -115,6 +115,7 @@ internal class JsonTreeDiffer( } return strippedTagDiffRows.mapIndexed { index, diffRow -> + println("Diff: Index: $index, $diffRow") when(diffRow.tag) { DiffRow.Tag.EQUAL -> { val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) @@ -221,7 +222,7 @@ internal class JsonTreeDiffer( return result } - private val inlineDiffTagOpen = "<$$$$>" - private val inlineDiffTagClosed = "" + private val inlineDiffTagOpen = "!!$$$$!!" + private val inlineDiffTagClosed = "!!/$$$$!!" } diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt index c43f76c..a8bc011 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt @@ -1,7 +1,9 @@ package com.sebastianneubauer.jsontree.diff +import androidx.compose.runtime.Immutable import com.sebastianneubauer.jsontree.JsonTreeElement +@Immutable internal sealed interface JsonTreeDifferState { data object Loading: JsonTreeDifferState @@ -11,11 +13,13 @@ internal sealed interface JsonTreeDifferState { val revisedJsonDiffElements: List ): JsonTreeDifferState + @Immutable sealed interface Error: JsonTreeDifferState { data class OriginalJsonError(val throwable: Throwable): Error data class RevisedJsonError(val throwable: Throwable): Error } + @Immutable sealed interface JsonDiffElement{ data class Change( val jsonTreeElement: JsonTreeElement, diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt index bc84ced..cf9d69a 100644 --- a/sample/src/commonMain/kotlin/App.kt +++ b/sample/src/commonMain/kotlin/App.kt @@ -44,11 +44,14 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sebastianneubauer.jsontree.JsonTree -import com.sebastianneubauer.jsontree.diff.SideBySideDiff2 import com.sebastianneubauer.jsontree.TreeColors import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.defaultDarkColors import com.sebastianneubauer.jsontree.defaultLightColors +import com.sebastianneubauer.jsontree.diff.JsonTreeDiff +import com.sebastianneubauer.jsontree.diff.JsonTreeDiffError +import com.sebastianneubauer.jsontree.diff.defaultDarkDiffColors +import com.sebastianneubauer.jsontree.diff.defaultLightDiffColors import com.sebastianneubauer.jsontree.search.rememberSearchState import com.sebastianneubauer.jsontreesample.sample.generated.resources.Res import com.sebastianneubauer.jsontreesample.sample.generated.resources.arrow_down @@ -96,6 +99,7 @@ private fun MainScreen() { var showIndices: Boolean by remember { mutableStateOf(true) } var showItemCount: Boolean by remember { mutableStateOf(true) } var expandSingleChildren: Boolean by remember { mutableStateOf(true) } + var showDiff: Boolean by remember { mutableStateOf(false) } val searchState = rememberSearchState() val searchQuery by remember(searchState.query) { mutableStateOf(searchState.query.orEmpty()) } val coroutineScope = rememberCoroutineScope() @@ -172,6 +176,13 @@ private fun MainScreen() { ) { Text(text = if (expandSingleChildren) "Expand children" else "Don't expand children") } + + Button( + modifier = Modifier.padding(horizontal = 8.dp), + onClick = { showDiff = !showDiff } + ) { + Text(text = if (showDiff) "Hide diff" else "Show diff") + } } Spacer(Modifier.height(8.dp)) @@ -243,7 +254,12 @@ private fun MainScreen() { Spacer(Modifier.height(8.dp)) - SideBySideDiff2() + if(showDiff) { + JsonDiff( + originalJson = json, + colors = colors, + ) + } Spacer(Modifier.height(8.dp)) @@ -332,6 +348,54 @@ private fun MainScreen() { } } +@Composable +private fun JsonDiff( + originalJson: String, + colors: TreeColors +) { + var error by remember { mutableStateOf(null) } + val errorMessage = error + + if (errorMessage != null) { + Text( + modifier = Modifier + .fillMaxSize() + .background( + color = if (colors == defaultLightColors) Color.Unspecified else Color.Black + ), + text = errorMessage, + color = if (colors == defaultLightColors) Color.Black else Color.White, + ) + } else { + JsonTreeDiff( + modifier = Modifier + .fillMaxWidth() + .height(400.dp), + originalJson = originalJson, + revisedJson = complexJsonRevised, + onLoading = { + Box( + modifier = Modifier + .fillMaxSize() + .background( + if (colors == defaultLightColors) Color.Unspecified else Color.Black + ), + contentAlignment = Alignment.Center + ) { + Text( + text = "Loading...", + color = if (colors == defaultLightColors) Color.Black else Color.White + ) + } + }, + colors = if(colors == defaultLightColors) defaultLightDiffColors else defaultDarkDiffColors, + onError = { + error = it.throwable.message + } + ) + } +} + @Preview @Composable private fun PreviewMainScreen() = JsonTreeTheme { diff --git a/sample/src/commonMain/kotlin/JsonTreeDiffStrings.kt b/sample/src/commonMain/kotlin/JsonTreeDiffStrings.kt new file mode 100644 index 0000000..e0f09b1 --- /dev/null +++ b/sample/src/commonMain/kotlin/JsonTreeDiffStrings.kt @@ -0,0 +1,2108 @@ +package com.sebastianneubauer.jsontreesample + +internal val complexJsonRevised = """ + { + "ctRoot": [ + { + "_id": "P4PFCX0HLYH3BFI6", + "name": "Ahmed Doyle", + "dob": "2020-02-11", + "addres": { + "street": "8749 Weston Road", + "town": "Chichester", + "postode": "SR88 4OA" + }, + "telephone": "+503-238-224-44509", + "pets": [ + "SUGAR" + ], + "score": 2.8, + "email": "cecille-whatley84@logical.com", + "url": "https://user.com", + "description": "bargains independence smell sharing electric extra failures wallpaper freelance higher mathematics disaster directed clicking elder anyone encountered living mattress drill", + "verified": true, + "salary": "38775", + "newValue": null + }, + { + "_id": "QRNEFG1GPFI5PJR9", + "name": "Gabriela Conaway", + "dob": "2015-07-23", + "address": { + "street": "4822 Troon Circle", + "town": "Swanscombe and Greenhithe", + "postode": "M0 3QW" + }, + "telephone": "+46-0620-673-252", + "pets": [ + "Charlie", + "Duke" + ], + "score": 3.6, + "email": "marvis01@florist.com", + "url": "http://sigma.kira.aichi.jp", + "description": "distributors desktop istanbul most contemporary fifteen pointing requesting adaptive wearing soft ap alcohol switch retreat blair fee dosage join mars", + "verified": true, + "salary": 30479 + }, + { + "_id": "0VUSDEIHR1KAT6ZR", + "name": "Leighann Luong", + "dob": "2023-03-31", + "address": { + "street": "9006 Fairbourne Street", + "town": "Otley", + "postode": "KA3 8JW" + }, + "telephone": "+98-5045-789-362", + "pets": [ + "Angel", + "Gus" + ], + "score": 4.1, + "email": "melodee8656@mtv.com", + "url": "https://mixer.cuiaba.br", + "description": "reload alternatively gras centuries math strips strict algorithms ethics taking mj holly rw friday disease chapel pgp helps bundle welcome", + "verified": true, + "salary": 54210 + }, + { + "_id": "sdfkfjoijwelj", + "name": "Inserted User", + "dob": "2024-03-31", + "address": { + "street": "9006 Fairbourne Street", + "town": "Otley", + "postode": "KA3 8JW" + }, + "telephone": "+98-5045-789-362", + "pets": [ + "Angel", + "Gus" + ], + "score": 4.1, + "email": "melodee8656@mtv.com", + "url": "https://mixer.cuiaba.br", + "description": "reload alternatively gras centuries math strips strict algorithms ethics taking mj holly rw friday disease chapel pgp helps bundle welcome", + "verified": true, + "salary": 54210 + }, + { + "_id": "MQXQR2KFEN97EAO3", + "name": "Cherilyn Radford", + "dob": "2019-12-08", + "address": { + "street": "6250 Norman Avenue", + "town": "Royal Leamington Spa", + "postode": "WA45 4SE" + }, + "telephone": "+358-0845-573-498", + "pets": [ + "Kiki", + "Bailey" + ], + "score": 2.2, + "email": "hilda.callahan@episode.com", + "url": "http://bufing.com", + "description": "devil crime does isa rental banks helmet rent artistic jerry purposes delayed failures sao outside loops buses hispanic attorney matthew", + "verified": false, + "salary": 11434 + }, + { + "_id": "MKREUCH3SZFPKTJQ", + "name": "King Nabors", + "dob": "2020-11-17", + "address": { + "street": "2240 Coldalhurst Road", + "town": "Tadley", + "postode": "ME6 2ZB" + }, + "telephone": "+53-8837-073-223", + "pets": [ + "Misty", + "Buddy" + ], + "score": 8.8, + "email": "buddy-fraley6468@yahoo.com", + "url": "https://www.guard.com", + "description": "rep mod agricultural closing av cv away loop disputes valuation undertaken friday weapon faqs knew ads fascinating administrators permission shortly", + "verified": true, + "salary": 62582 + }, + { + "_id": "GU7TYOG0GDJPJI9T", + "name": "Rusty Doughty", + "dob": "2018-10-30", + "address": { + "street": "5380 Brantwood Street", + "town": "South Benfleet", + "postode": "S8 3OT" + }, + "telephone": "+33-8002-990-182", + "pets": [ + "Lilly", + "Duke" + ], + "score": 2.5, + "email": "jaymie-mejia-rodrigue90@gmail.com", + "url": "https://dishes.com", + "description": "likely convert tba diameter pressed tooth electron concerns plug blvd javascript dh melbourne recover compile justify d principle banners context", + "verified": true, + "salary": 16751 + }, + { + "_id": "6P8MKM7L1G785V1P", + "name": "Scotty Freeman", + "dob": "2017-12-30", + "address": { + "street": "0910 Whitfield", + "town": "Scone", + "postode": "AL7 3BN" + }, + "telephone": "+65-1463-421-043", + "pets": [ + "CoCo", + "Bear" + ], + "score": 1.6, + "email": "geneva.babb7@livestock.com", + "url": "http://www.invitations.com", + "description": "bradford substitute majority satisfy outdoors impression licensing treated floppy examined smell trainers adrian legs myth account agrees frame containing gbp", + "verified": true, + "salary": 67828 + }, + { + "_id": "QDCYQXXJIVGNS4RU", + "name": "Letha Abell", + "dob": "2019-09-20", + "address": { + "street": "7700 Ellenor Circle", + "town": "Castleford", + "postode": "CT2 8VT" + }, + "telephone": "+62-4664-033-388", + "pets": [ + "Sammy", + "Lexi" + ], + "score": 6.5, + "email": "theresa497@remove.com", + "url": "http://www.da.com", + "description": "intl veterans tap tanzania belong warranty crafts consumption posting risk unix tulsa gregory crossing based delete hairy presentations walter newman", + "verified": false, + "salary": 41762 + }, + { + "_id": "IQZ878RHHO2X54KN", + "name": "Nubia Merriman", + "dob": "2021-06-14", + "address": { + "street": "8913 Runger", + "town": "Rackheath", + "postode": "CB06 9UJ" + }, + "telephone": "+251-5498-951-264", + "pets": [ + "Callie", + "Jack" + ], + "score": 6.7, + "email": "charlotteflanagan177@cart.com", + "url": "https://hunger.dj", + "description": "wayne affiliation days phys trustees pediatric fiji thunder attached possibility tulsa ethernet whilst fight santa merchants fx lu preferred pads", + "verified": true, + "salary": 63386 + }, + { + "_id": "2EZUVZQQHGM3ET8E", + "name": "Shad Burchfield", + "dob": "2018-07-27", + "address": { + "street": "7288 Lackford Avenue", + "town": "Rainham", + "postode": "EN0 6UG" + }, + "telephone": "+33-1765-906-835", + "pets": [ + "Peanut", + "Oliver" + ], + "score": 6.9, + "email": "suzan_foley37491@refresh.com", + "url": "https://www.glance.com", + "description": "photos soul humanities policies techrepublic estate commodity add media lamb dress testimony authority earliest samba bean bike watt yugoslavia healthy", + "verified": true, + "salary": 13253 + }, + { + "_id": "7H72LP449JE0QZVN", + "name": "Hyacinth Beard", + "dob": "2020-03-26", + "address": { + "street": "6878 Burstead", + "town": "Brentwood", + "postode": "S86 2TE" + }, + "telephone": "+36-9587-022-215", + "pets": [ + "Dusty", + "Oliver" + ], + "score": 6.3, + "email": "sharyl7852@pitch.com", + "url": "http://www.guyana.com", + "description": "precipitation ladder roommates satin dude celebs fp regards sponsors surprised containing combinations event jr offices bits lakes florist enable establishment", + "verified": false, + "salary": 27809 + }, + { + "_id": "AJ3PO7XDAF64S59F", + "name": "Nada Hathaway", + "dob": "2014-05-11", + "address": { + "street": "6904 Farnsworth Avenue", + "town": "Nottingham", + "postode": "HU0 3VF" + }, + "telephone": "+593-1858-838-385", + "pets": [ + "Callie", + "Ginger" + ], + "score": 9.6, + "email": "brant82413@prescription.com.ni", + "url": "https://fireplace.com", + "description": "phoenix applicable serial find major guru genre copying expensive o premium li belarus botswana thermal deliver bradley dialog league victorian", + "verified": true, + "salary": 56699 + }, + { + "_id": "RDFHOYYIYQEZY9KL", + "name": "Kristen Stevens", + "dob": "2022-05-18", + "address": { + "street": "9387 Ena", + "town": "Little Coates", + "postode": "SW78 0OE" + }, + "telephone": "+39-0641-092-168", + "pets": [ + "Oliver", + "Jack" + ], + "score": 8, + "email": "dorthea.segura@hotmail.com", + "url": "https://loaded.com", + "description": "ba offers precious broadcasting invasion victor literally updates accountability aka accomplished builds da cycling oil corruption mesh ja peak jeep", + "verified": true, + "salary": 37550 + }, + { + "_id": "8Y19A0D8VYP4VQHL", + "name": "Natosha Regan", + "dob": "2022-11-21", + "address": { + "street": "5616 Shirebrook", + "town": "Lechlade", + "postode": "HG09 7QQ" + }, + "telephone": "+45-5698-414-301", + "pets": [ + "Belle", + "Sam" + ], + "score": 3.8, + "email": "ezequiel_tiller693@votes.com", + "url": "https://www.buyer.com", + "description": "align presenting codes contracts zoloft cigarettes impaired dolls linear counseling perhaps circulation das anna adequate funeral broadband sync grad arrival", + "verified": true, + "salary": 35041 + }, + { + "_id": "FZXX9S1YEXUE7QXT", + "name": "Evangelina Worrell", + "dob": "2015-02-23", + "address": { + "street": "1142 Vantomme Road", + "town": "Langley Mill", + "postode": "FY5 8PC" + }, + "telephone": "+266-4488-227-054", + "pets": [ + "Rusty", + "Dexter" + ], + "score": 8.6, + "email": "concha-long10@yahoo.com", + "url": "http://www.dan.com", + "description": "reef mental angle springs dying shipped minimum princess resume speaker ciao substantial specify deck casting killed stamp bo stages fellowship", + "verified": false, + "salary": 14350 + }, + { + "_id": "FOIIP6V5QJ73TGFE", + "name": "Mamie Ferreira", + "dob": "2015-04-25", + "address": { + "street": "3916 Wareing Lane", + "town": "Comrie", + "postode": "CV0 2JN" + }, + "telephone": "+47-0368-483-623", + "pets": [ + "Chloe", + "Ruby" + ], + "score": 3.2, + "email": "diegolayman@gmail.com", + "url": "https://www.executed.com", + "description": "puts hello packets az messages pike pam contributors chrysler fp ps monetary berry renewal edt per muslim penalties virginia remaining", + "verified": true, + "salary": 23964 + }, + { + "_id": "JY4KQ7ZLP93YBFTG", + "name": "Taina Brandenburg", + "dob": "2019-04-27", + "address": { + "street": "7847 Basten", + "town": "Stocksbridge", + "postode": "TA0 7QB" + }, + "telephone": "+268-1926-217-192", + "pets": [ + "Simba", + "Cody" + ], + "score": 6.2, + "email": "classie-sage8@manufacturers.com", + "url": "https://stayed.com", + "description": "genetic begin ia queens adapters uganda owner jonathan retired discount knit authorization certain applicants plugins promotions attention our specs rochester", + "verified": true, + "salary": 23218 + }, + { + "_id": "YMIO6BZZ5LTR51TP", + "name": "Eun Ward", + "dob": "2021-09-06", + "address": { + "street": "4932 Farrell Avenue", + "town": "Dollar", + "postode": "DG59 9YW" + }, + "telephone": "+64-2367-673-643", + "pets": [ + "Pumpkin", + "Shadow" + ], + "score": 7.7, + "email": "selena_wiese22901@gmail.com", + "url": "http://www.map.com", + "description": "acre charleston louisville colleges beverly por outline japanese hart hp ancient fort bizarre van barbie worse plasma wp belts strength", + "verified": false, + "salary": 67665 + }, + { + "_id": "C4U18R49EZIGPB2Z", + "name": "Dexter Bonner", + "dob": "2015-10-29", + "address": { + "street": "3864 Rostron Road", + "town": "Hatfield", + "postode": "GU3 4LQ" + }, + "telephone": "+268-3236-580-437", + "pets": [ + "Peanut", + "Ginger" + ], + "score": 1.2, + "email": "genie_hendricks979@les.takino.hyogo.jp", + "url": "http://www.midi.com", + "description": "feeling close mc wayne estimates purpose classic opponent most brothers ham bingo nottingham and adam prerequisite doom besides thailand lane", + "verified": true, + "salary": 28137 + }, + { + "_id": "56MXBNM12FATLNVI", + "name": "Alvaro Villarreal", + "dob": "2023-05-24", + "address": { + "street": "7364 Lingards Circle", + "town": "Patchway", + "postode": "WD35 0ZA" + }, + "telephone": "+670-2918-496-652", + "pets": [ + "Dusty", + "Buddy" + ], + "score": 5.7, + "email": "kathryne_shanks@leisure.com", + "url": "http://trying.com", + "description": "indians martin exports stomach field chester accounting lu orbit reply saturn viewers begin generating she personnel electrical dp lines light", + "verified": true, + "salary": 55703 + }, + { + "_id": "JHAJQ5S0JVPLFQJQ", + "name": "Jonas Roberson", + "dob": "2015-01-16", + "address": { + "street": "9460 Greenleach Road", + "town": "Bude", + "postode": "NN31 9IG" + }, + "telephone": "+506-1959-085-739", + "pets": [ + "Felix", + "Teddy" + ], + "score": 1.9, + "email": "elina.gallegos386@yahoo.com", + "url": "http://www.wu.com", + "description": "responsibilities walk commitments equity uc chicken domains lands mrna obtained journals exam tom witnesses xanax aluminum dinner screensavers guild prozac", + "verified": true, + "salary": 56932 + }, + { + "_id": "BQ6SOG974MD8N828", + "name": "Barbra Kenney", + "dob": "2016-12-28", + "address": { + "street": "2919 Viewlands Road", + "town": "Worsley", + "postode": "G02 9PE" + }, + "telephone": "+591-9283-059-613", + "pets": [ + "Baby", + "Emma" + ], + "score": 8.1, + "email": "cheryll_sallee@yahoo.com", + "url": "http://www.sorted.com", + "description": "luis situations provision customized similarly previously wisconsin mounting sessions sport partners corporations creation j gender floating like lowest genetics consistently", + "verified": false, + "salary": 66137 + }, + { + "_id": "2C2K6K4AEELQ2FG6", + "name": "Julia Tenney", + "dob": "2014-11-09", + "address": { + "street": "0568 Fairlands Road", + "town": "Dunwich", + "postode": "EH87 9DY" + }, + "telephone": "+351-0880-127-685", + "pets": [ + "Dexter", + "Stella" + ], + "score": 4.1, + "email": "ilse.solomon6799@marriage.us-east-1.amazonaws.com", + "url": "https://www.expiration.com", + "description": "arising longitude theme radio flexibility informational geek wealth sister hospital racks feat chemistry cars mod wheat futures gcc belle onto", + "verified": true, + "salary": 42470 + }, + { + "_id": "QA59QZ78Q6OOXAXN", + "name": "Chara Broughton", + "dob": "2020-02-15", + "address": { + "street": "0949 Crossley", + "town": "Garforth", + "postode": "HS9 7TD" + }, + "telephone": "+36-0863-877-518", + "pets": [ + "Luna", + "Max" + ], + "score": 1, + "email": "titus-tibbetts110@gmail.com", + "url": "http://www.closest.com", + "description": "tracker spots ram and vancouver robin gary disciplinary practitioner ka negotiation considerations holders arm july signal lack knives queue bond", + "verified": true, + "salary": 13084 + }, + { + "_id": "DT6IX195MKMD8S17", + "name": "Vasiliki Ely-Archuleta", + "dob": "2018-04-16", + "address": { + "street": "1188 Manston Circle", + "town": "Lincoln", + "postode": "DY2 2UG" + }, + "telephone": "+962-2391-907-621", + "pets": [ + "Toby", + "Marley" + ], + "score": 3.4, + "email": "hans631@gmail.com", + "url": "http://www.belly.com", + "description": "memo lost brunswick teaches screenshot tail potato overview commitment batman railway jul affecting ace bristol thrown taste dedicated turkey crimes", + "verified": false, + "salary": 68202 + }, + { + "_id": "ATQ4ZZPGZFJGQ9TU", + "name": "Ashlee Finch", + "dob": "2017-07-25", + "address": { + "street": "7187 Lingmoor", + "town": "Old Colwyn", + "postode": "SO9 3SJ" + }, + "telephone": "+33-8497-151-774", + "pets": [ + "Cleo", + "Jax" + ], + "score": 5.8, + "email": "shakiabohannon3250@knights.mil.qa", + "url": "https://logan.com", + "description": "dramatic leather subscribe joins alabama service partly laser arbitration parliament hughes councils suites advert sucking extraction islam essentials born insulation", + "verified": false, + "salary": 32582 + }, + { + "_id": "R0DVSIYMGEE3JT1K", + "name": "Lecia Causey", + "dob": "2014-12-19", + "address": { + "street": "7421 Gray Avenue", + "town": "Winchelsea", + "postode": "WF74 3CW" + }, + "telephone": "+53-8245-701-609", + "pets": [ + "sox", + "Jake" + ], + "score": 10, + "email": "mavis6493@granny.com", + "url": "https://www.engagement.com", + "description": "stuffed theaters ultimate tactics intimate morris roots hawaiian maiden lawrence theaters sample rapids gp prefer teaching mount phillips anatomy districts", + "verified": true, + "salary": 27840 + }, + { + "_id": "YN0E0M3N1JU1Q4BS", + "name": "Margeret Michaud", + "dob": "2020-10-22", + "address": { + "street": "7693 Woodlea", + "town": "Dagenham", + "postode": "CB2 8AI" + }, + "telephone": "+502-5878-681-419", + "pets": [ + "Lily", + "Bear" + ], + "score": 1.2, + "email": "merlene-biddle9@ind.pro.mv", + "url": "https://www.cleaning.beskidy.pl", + "description": "includes alike death disco route consequence polls dam sight stanford workstation amy attorneys lies creator charms stockholm credit singing inexpensive", + "verified": false, + "salary": 68543 + }, + { + "_id": "TJHT99FASP2Q2ES4", + "name": "Felice Glaze", + "dob": "2023-02-15", + "address": { + "street": "3623 Selstead Road", + "town": "Cockermouth", + "postode": "RM35 4WF" + }, + "telephone": "+53-1387-463-684", + "pets": [ + "Sebastian", + "Emma" + ], + "score": 3.9, + "email": "deshawnbeckett46353@gmail.com", + "url": "http://applicant.com", + "description": "executive ex fc trouble celtic pamela different repair parker warrior arkansas expires spanish authorities representation template partial flexible pass briefing", + "verified": false, + "salary": 21742 + }, + { + "_id": "0JYP33K2BSY0F4MK", + "name": "Isela Tabor-Norris", + "dob": "2021-09-26", + "address": { + "street": "3362 Vickers Avenue", + "town": "Jarrow", + "postode": "DT4 0MD" + }, + "telephone": "+61-3637-045-692", + "pets": [ + "CoCo", + "Jax" + ], + "score": 2, + "email": "robby-yates@yahoo.com", + "url": "http://examinations.com", + "description": "telling jamie dish witnesses you riders rent approach astrology traditions runner decent heater heights disks porter jason rv chemicals lancaster", + "verified": true, + "salary": 49587 + }, + { + "_id": "YSA4H3D79VZH2Q1I", + "name": "Riley Pratt", + "dob": "2021-04-10", + "address": { + "street": "9029 Carnoustie Street", + "town": "Oundle", + "postode": "TW2 2MV" + }, + "telephone": "+66-1890-697-938", + "pets": [ + "Pepper", + "Apollo" + ], + "score": 9.7, + "email": "annabelle-nelson-sanders9984@effects.com", + "url": "http://swiss.com", + "description": "inline handled president pot mineral media channel mobiles switched coffee sister handled vs laughing suse whats cards papers nebraska ave", + "verified": false, + "salary": 43774 + }, + { + "_id": "C2JRIC5XMV2XQDYN", + "name": "Akilah Culpepper", + "dob": "2014-11-13", + "address": { + "street": "4687 Teneriffe Street", + "town": "Penwortham", + "postode": "KT20 4SK" + }, + "telephone": "+965-0776-334-101", + "pets": [ + "Noodle", + "Dexter" + ], + "score": 6.5, + "email": "darren9@gmail.com", + "url": "http://came.com", + "description": "bored records chinese failure temperatures nature reality websites assigned florist passport deployment actions promotion sources force dp atmosphere olive genesis", + "verified": true, + "salary": 55373 + }, + { + "_id": "5ZT6VBJD4862OVQC", + "name": "Kristina Stover", + "dob": "2021-09-13", + "address": { + "street": "7897 Ludgate Avenue", + "town": "Haddington", + "postode": "HX5 9YD" + }, + "telephone": "+66-5753-813-686", + "pets": [ + "Frankie", + "Ginger" + ], + "score": 2.2, + "email": "nannie.whatley475@hotmail.com", + "url": "http://suitable.com", + "description": "bold ipaq proprietary relax suitable superior rep pose dan indexed texts joseph gods cornell unity remarkable cohen grounds millions fail", + "verified": true, + "salary": 20746 + }, + { + "_id": "NIVBMYQIJE0JHQE3", + "name": "Ivory Hager", + "dob": "2018-09-19", + "address": { + "street": "0900 Partington Street", + "town": "St Clears", + "postode": "NG45 9YY" + }, + "telephone": "+973-2068-107-573", + "pets": [ + "Jasper", + "Stella" + ], + "score": 2.3, + "email": "kaceynobles@suppose.com", + "url": "https://probability.com", + "description": "portions neighbor delicious generate simulations ohio fa ferrari directive electricity princess gotta temporal consistent relying soon moms registration bolivia suburban", + "verified": true, + "salary": 35154 + }, + { + "_id": "8648RK4LCOKYFAHN", + "name": "Keesha Spearman", + "dob": "2020-01-27", + "address": { + "street": "2283 Marchwood", + "town": "Portishead and North Weston", + "postode": "DG91 1CF" + }, + "telephone": "+266-9212-632-553", + "pets": [ + "mittens", + "Ellie" + ], + "score": 4.2, + "email": "hettie1749@text.com", + "url": "http://www.controlling.com", + "description": "displays aim shoot strategy anything affairs wb subsection investigation focusing pearl replace recovered would seventh ink land bishop demonstrates authorization", + "verified": true, + "salary": 23874 + }, + { + "_id": "0BEFY8L7FRJBDSUU", + "name": "Nola Heim", + "dob": "2017-01-21", + "address": { + "street": "9127 Westmorland Circle", + "town": "Conwy", + "postode": "SG8 7MP" + }, + "telephone": "+671-0294-820-870", + "pets": [ + "boo", + "Lilly" + ], + "score": 6.3, + "email": "justina40@raising.com", + "url": "https://customized.com", + "description": "analysis spain moving contacts kay assembled millions weak athens kernel bicycle ds upset memorabilia crew bradley tight disability projects newest", + "verified": true, + "salary": 38215 + }, + { + "_id": "QQEYFJD6OSTQBEKY", + "name": "Kimbery Kim", + "dob": "2019-09-16", + "address": { + "street": "3064 Kingston Street", + "town": "Yeadon", + "postode": "SS7 8UO" + }, + "telephone": "+92-3678-298-125", + "pets": [ + "Murphy", + "Bear" + ], + "score": 4.7, + "email": "rettabowden8400@gmail.com", + "url": "https://mustang.com", + "description": "keen occurring chick optical shipping somerset techno deer discrimination anchor ot ears maker picnic wrap flour addition huge advert reports", + "verified": true, + "salary": 41554 + }, + { + "_id": "YM36I083BP15HA4V", + "name": "Francesca Wicks", + "dob": "2017-03-05", + "address": { + "street": "8737 Chetwyn Circle", + "town": "Inverness", + "postode": "G9 6YX" + }, + "telephone": "+34-4181-651-414", + "pets": [ + "Sophie", + "Dexter" + ], + "score": 3.7, + "email": "vitobeeson6181@yahoo.com", + "url": "https://www.pamela.com", + "description": "sewing civic itself circle pottery biology trinidad printer often task ye mhz plot performing land radius swim rack wb plugin", + "verified": false, + "salary": 58615 + }, + { + "_id": "QQHAQITNR2VSX33Q", + "name": "Ilene Ames", + "dob": "2017-07-10", + "address": { + "street": "7598 Booths Road", + "town": "Featherstone", + "postode": "DE07 5UX" + }, + "telephone": "+66-4042-102-932", + "pets": [ + "tucker", + "Marley" + ], + "score": 5.7, + "email": "sherrill59@hotmail.com", + "url": "http://knee.com", + "description": "copies guilty therapy ghana join skip anyone incidence point website levels modular nikon beverly vietnam dynamics tu wrist founder structured", + "verified": true, + "salary": 66294 + }, + { + "_id": "Q6SV56K6UGKAPQM3", + "name": "Lakeshia Martindale", + "dob": "2020-02-28", + "address": { + "street": "1985 Ashley Avenue", + "town": "Redditch", + "postode": "WD1 2MZ" + }, + "telephone": "+503-0760-223-380", + "pets": [ + "Lola", + "Mia" + ], + "score": 9.2, + "email": "jeanie_weeks72041@hotmail.com", + "url": "https://ep.com", + "description": "expect reaction holly remark jason download matters stockholm completed laughing fcc mobiles trains baseline crossword yn il shanghai closes indonesian", + "verified": true, + "salary": 16869 + }, + { + "_id": "02BIDLJG194QQI9Y", + "name": "Ryan Washington", + "dob": "2020-11-25", + "address": { + "street": "1726 Balliol", + "town": "Whitstable", + "postode": "IV6 2IC" + }, + "telephone": "+592-6275-136-493", + "pets": [ + "Boots", + "Zeus" + ], + "score": 9.9, + "email": "angila.winchester2135@hotmail.com", + "url": "http://www.dip.com", + "description": "maine vatican cry romantic recycling mazda ronald double designers chubby moscow synthetic administrative fleece workplace dell freebsd gentle ep baking", + "verified": true, + "salary": 53312 + }, + { + "_id": "A8ZYBQX02O4622AV", + "name": "Zachariah Carnes", + "dob": "2021-09-15", + "address": { + "street": "4283 Bridson Avenue", + "town": "Falmouth", + "postode": "EC79 6PE" + }, + "telephone": "+33-9740-058-093", + "pets": [ + "Max", + "Buddy" + ], + "score": 1, + "email": "cary_martel@follows.com", + "url": "https://class.com", + "description": "eligibility advancement football refers typical sonic cuba calm pacific wow park extra bat viruses educated offices sony deficit lat radical", + "verified": true, + "salary": 46705 + }, + { + "_id": "S72XP117QTS9PGXL", + "name": "Kraig Hayden", + "dob": "2018-11-24", + "address": { + "street": "9592 Darwen Road", + "town": "Wotton under Edge", + "postode": "WN45 1MN" + }, + "telephone": "+350-8288-714-892", + "pets": [ + "Princess", + "Nala" + ], + "score": 7.6, + "email": "thaddeus87417@gmail.com", + "url": "http://seats.com", + "description": "nba depression spirits henderson individual removable cn database tissue cinema consultants dock around usr bones attached emerging payable index group", + "verified": true, + "salary": 16004 + }, + { + "_id": "K36SZLAATH9U32E2", + "name": "Brooke Hyde", + "dob": "2021-04-13", + "address": { + "street": "8850 Crofton Circle", + "town": "Portrush", + "postode": "TR8 9JG" + }, + "telephone": "+39-3139-767-272", + "pets": [ + "Misty", + "Rosie" + ], + "score": 2.3, + "email": "barry336@gmail.com", + "url": "https://pleased.com", + "description": "pics exceptional covers produces flex net cached raid startup mattress artist locked newer weekend reset infectious sit livecam percent hosting", + "verified": true, + "salary": 29650 + }, + { + "_id": "3RM3XBQ0S5YMRRPY", + "name": "Ilona Gantt", + "dob": "2021-05-01", + "address": { + "street": "0799 Eden Road", + "town": "Hayle", + "postode": "LN03 2OS" + }, + "telephone": "+82-0237-770-488", + "pets": [ + "Max", + "Lilly" + ], + "score": 1.8, + "email": "shirley.mattox14332@yahoo.com", + "url": "http://www.condos.com", + "description": "teeth gallery asking reads sa faith right seasons farms respected automotive examples tour mood anderson scary sites clone grow sisters", + "verified": false, + "salary": 67087 + }, + { + "_id": "OYPLI143FY2DATHU", + "name": "Gustavo Newberry", + "dob": "2017-02-14", + "address": { + "street": "5004 Winifred Street", + "town": "Littlehampton", + "postode": "GL45 3AF" + }, + "telephone": "+260-1892-979-433", + "pets": [ + "Boots", + "Shadow" + ], + "score": 1.6, + "email": "norma3@somerset.com", + "url": "http://pair.com", + "description": "virgin thing dicke alexander powerpoint recordings specific spoken cases fitness enjoyed cloth ends scott coordinated beginning magnetic behind gives generations", + "verified": true, + "salary": 40316 + }, + { + "_id": "7BH73QUUGQEQMKPY", + "name": "Roscoe Seeley", + "dob": "2016-05-06", + "address": { + "street": "4591 Brackenhurst Circle", + "town": "Lytham St Annes", + "postode": "GL17 8EX" + }, + "telephone": "+231-3940-507-401", + "pets": [ + "Lilly", + "Cody" + ], + "score": 4.5, + "email": "latia15931@syndication.sondrio.it", + "url": "http://beam.com", + "description": "mayor holland disputes entering barbados monsters receiver electoral banana oldest ships your copying comfortable comparative knowledge june retain gear nbc", + "verified": true, + "salary": 27929 + }, + { + "_id": "QLZM37KZGMCIQMPL", + "name": "Tuan Bratton", + "dob": "2021-04-24", + "address": { + "street": "8073 Stretford", + "town": "Dumbarton", + "postode": "HX5 2KI" + }, + "telephone": "+45-5887-998-306", + "pets": [ + "Misty", + "Harley" + ], + "score": 9.6, + "email": "carleen.treat@gmail.com", + "url": "http://www.engaged.com", + "description": "tier foods completely phil shaft follow shelter offline extraction membrane hop super creations medicine photos sleeping intense ht excitement instruction", + "verified": true, + "salary": 50465 + }, + { + "_id": "84TJD43FO2OYD4GO", + "name": "Bridgette Trent", + "dob": "2023-12-04", + "address": { + "street": "0087 Cromdale Lane", + "town": "Portsmouth", + "postode": "PO58 1WY" + }, + "telephone": "+46-3453-741-464", + "pets": [ + "Fiona", + "Sasha" + ], + "score": 7.1, + "email": "elois.hoppe@provinces.trentino-altoadige.it", + "url": "http://examining.com", + "description": "cosmetic alt mysql give testing implement reception forward synthetic creature trail adsl languages trauma greek twenty defendant served blind jpeg", + "verified": false, + "salary": 47734 + }, + { + "_id": "7A10CRNKUJZ0NKJ1", + "name": "Jeanelle Brower", + "dob": "2016-05-21", + "address": { + "street": "4916 Blackwood", + "town": "Stroud", + "postode": "B00 0IM" + }, + "telephone": "+592-5201-551-021", + "pets": [ + "Rusty", + "Apollo" + ], + "score": 3.5, + "email": "sadielandry@brook.com", + "url": "https://www.ross.com", + "description": "who ef answered street rotation boats podcast governance rehabilitation consultation while spirituality pencil one press person prior elected most ver", + "verified": true, + "salary": 27595 + }, + { + "_id": "OAS78JF34Z1FVP7G", + "name": "Roxane Sierra", + "dob": "2015-10-07", + "address": { + "street": "9624 Hollins Street", + "town": "North Shields", + "postode": "NW8 4SK" + }, + "telephone": "+358-0580-708-034", + "pets": [ + "Scooter", + "Buddy" + ], + "score": 8.1, + "email": "eusebiarivero7653@yahoo.com", + "url": "https://www.shipped.com", + "description": "collect addresses someone generating powers heavily utc annually covered trail qualifying fl participated logged hp g scientific coffee ml tough", + "verified": false, + "salary": 44784 + }, + { + "_id": "PJZIKBB5UGQY680G", + "name": "Rudolph Johns", + "dob": "2020-08-22", + "address": { + "street": "7398 Back", + "town": "Duniplace", + "postode": "KT29 8DE" + }, + "telephone": "+90-1120-937-946", + "pets": [ + "Milo", + "Nala" + ], + "score": 8, + "email": "remona.saucedo@holding.com", + "url": "http://subject.com", + "description": "influence idol forgot suspended grown concern zu children comment webpage breeds canon talking civil basically privilege boulder speed vegetarian eve", + "verified": true, + "salary": 64551 + }, + { + "_id": "FJ1QU3Y2RHS20ZM4", + "name": "Letty Davenport", + "dob": "2018-04-11", + "address": { + "street": "2546 Owlwood Road", + "town": "Saltburn by the Sea", + "postode": "HU03 3ZR" + }, + "telephone": "+886-9362-087-451", + "pets": [ + "Maggie", + "Mia" + ], + "score": 5.4, + "email": "betsy-boles45413@absence.com", + "url": "https://www.education.com", + "description": "mic mcdonald requiring macromedia fonts tribute rack substances dictionary release para blog section diversity processing leu avoid special line nextel", + "verified": false, + "salary": 51990 + }, + { + "_id": "LN68OIVNI8YEHBR1", + "name": "Neida Damico", + "dob": "2018-11-22", + "address": { + "street": "7796 Ladyshore Street", + "town": "Fenny Stratford", + "postode": "CO05 3VD" + }, + "telephone": "+260-2845-548-530", + "pets": [ + "Luna", + "Sam" + ], + "score": 8.5, + "email": "kanesha98@engine.com", + "url": "http://www.optimum.com", + "description": "ca circle superintendent distributor interactive civilization background molecules mp stripes reproduction calculated flux able us neighborhood russell suffered stored marble", + "verified": true, + "salary": 40763 + }, + { + "_id": "PK4EGSSL903PO18O", + "name": "Santana Augustine", + "dob": "2018-10-31", + "address": { + "street": "5476 Burran Road", + "town": "Aldridge", + "postode": "NR19 3JT" + }, + "telephone": "+42-8959-252-781", + "pets": [ + "Tiger", + "Bentley" + ], + "score": 2, + "email": "tuansummerlin@yahoo.com", + "url": "https://accused.com", + "description": "establish valued oo eagles qc blanket packages owns sand inspector theme potter gain need dan alerts exam epson ordinance incentives", + "verified": true, + "salary": 20832 + }, + { + "_id": "QRDQ5M6MA06ABIRO", + "name": "Rico Baum", + "dob": "2018-04-15", + "address": { + "street": "3533 Rowton", + "town": "Weston super Mare", + "postode": "B10 2UH" + }, + "telephone": "+505-1339-333-063", + "pets": [ + "George", + "Duke" + ], + "score": 1.4, + "email": "maragret_stowe253@criticism.com", + "url": "https://carter.com", + "description": "thumbnail broken ppc delight scenario cute ant bit sig requiring quoted rebel typical brush mission zealand map clouds elimination meetings", + "verified": true, + "salary": 55113 + }, + { + "_id": "Y2ZZSQ1SQ4H96ODM", + "name": "Lottie Hurt", + "dob": "2023-05-31", + "address": { + "street": "9896 Bulteel Street", + "town": "Dingwall", + "postode": "BS65 8AT" + }, + "telephone": "+675-1540-885-858", + "pets": [ + "MIMI", + "Jake" + ], + "score": 2.7, + "email": "jamika3@symptoms.com", + "url": "https://belarus.com", + "description": "planets nm divorce handles constitutional taxi cruz zdnet update selected complexity fiber ricky lingerie outlined spanking lover disabilities supreme advocate", + "verified": false, + "salary": 21390 + }, + { + "_id": "6IXCNIAPSLFB30L9", + "name": "Elsy Armenta", + "dob": "2022-11-17", + "address": { + "street": "2003 Crescent Road", + "town": "Ashby Woulds", + "postode": "FK70 8PJ" + }, + "telephone": "+351-8475-596-127", + "pets": [ + "Garfield", + "Roxy" + ], + "score": 5.1, + "email": "marion-jordan804@happen.com", + "url": "http://ot.com", + "description": "export ac loved invision controversial indians blast pichunter process iraq named pr flyer acquisition gym am trouble kitty fd hook", + "verified": true, + "salary": 57454 + }, + { + "_id": "FG24QMXTPQH6G6JG", + "name": "Sheridan Lockwood", + "dob": "2016-07-27", + "address": { + "street": "6625 Sulgrave Lane", + "town": "Earley", + "postode": "LL20 6ME" + }, + "telephone": "+225-8025-293-370", + "pets": [ + "Sadie", + "Buddy" + ], + "score": 3, + "email": "remona-windsor4089@barry.com", + "url": "http://www.vegetarian.com", + "description": "appointments arrested hop ruling packing assets jesse alloy assets fastest mothers determination mac appeared min src distance seems previously nickname", + "verified": false, + "salary": 66568 + }, + { + "_id": "937QER84SCCVFYLD", + "name": "Ginette Nagel", + "dob": "2022-10-11", + "address": { + "street": "7678 Everleigh Road", + "town": "South Benfleet", + "postode": "IP01 3PD" + }, + "telephone": "+90-8897-278-589", + "pets": [ + "George", + "Murphy" + ], + "score": 9.5, + "email": "danyel.kenney@yahoo.com", + "url": "http://www.anti.gov.sc", + "description": "collaboration alto lost cab robust elections cleared respectively funeral food loss extra protocols communication colours cruz open approval cut strikes", + "verified": true, + "salary": 10353 + }, + { + "_id": "60XY943Q3LQ01F63", + "name": "Phyliss Winkler", + "dob": "2014-08-30", + "address": { + "street": "1049 Gould", + "town": "Hornsea", + "postode": "WV19 0SM" + }, + "telephone": "+260-0804-882-014", + "pets": [ + "Leo", + "Bailey" + ], + "score": 5.9, + "email": "sharondareiss@sure.com", + "url": "http://www.intensive.com", + "description": "saskatchewan being pour kingston obj vb ceiling racing av increases attitude craig ag uniprotkb fill steven myspace christians al questionnaire", + "verified": true, + "salary": 60581 + }, + { + "_id": "PAJLBFMPDJC9YPR5", + "name": "Una Bowden", + "dob": "2018-03-13", + "address": { + "street": "1586 Grave Lane", + "town": "Motherwell", + "postode": "SG6 0TI" + }, + "telephone": "+971-1008-527-355", + "pets": [ + "Rusty", + "Scout" + ], + "score": 8.3, + "email": "mason59@gmail.com", + "url": "https://change.gildeskål.no", + "description": "efficiency odd essex ship zinc papers payday manitoba recycling against moore borders pit invasion lighting feels activists preference parks appointments", + "verified": true, + "salary": 67484 + }, + { + "_id": "T0JCRHV90E41SS91", + "name": "Nickolas Savage", + "dob": "2017-08-21", + "address": { + "street": "0727 Lymmington Street", + "town": "Kidwelly", + "postode": "RM2 0BT" + }, + "telephone": "+255-8832-671-094", + "pets": [ + "Misty", + "Henry" + ], + "score": 3.6, + "email": "aretha9@miscellaneous.stream", + "url": "https://www.selling.com", + "description": "social thats differences platform quoted release grocery copies traditions header vault accommodations vc therapeutic king caution gp connectivity dividend hook", + "verified": true, + "salary": 24303 + }, + { + "_id": "2ACDO84RDYZFCSML", + "name": "Hwa Frey", + "dob": "2023-03-09", + "address": { + "street": "7236 Riverside Lane", + "town": "Leyton", + "postode": "SS75 0AP" + }, + "telephone": "+45-8150-396-957", + "pets": [ + "cupcake", + "Jax" + ], + "score": 5.6, + "email": "ashli_berman@pty.com", + "url": "https://www.bhutan.com", + "description": "denial merchant than chips metadata weights drugs followed jesus ix edt geek establishment construct owen followed sensor bull s said", + "verified": true, + "salary": 28236 + }, + { + "_id": "AVGOBIJ6M0KB5QEH", + "name": "Ismael Duval", + "dob": "2017-09-30", + "address": { + "street": "5732 Meadway Avenue", + "town": "Mildenhall", + "postode": "SO9 8QG" + }, + "telephone": "+231-4889-238-803", + "pets": [ + "Angel", + "Lexi" + ], + "score": 1, + "email": "brande866@wisdom.com", + "url": "http://administered.com", + "description": "nancy aggregate japan genius portion tender replacement kruger will ultimate survivors sucking coordinated arizona hayes developments indication manager suit qc", + "verified": true, + "salary": 13366 + }, + { + "_id": "L72D6Y3T8QV5ICV7", + "name": "Rebeca Pomeroy", + "dob": "2016-01-18", + "address": { + "street": "8651 Whiteway Street", + "town": "Swanscombe and Greenhithe", + "postode": "HR07 9ID" + }, + "telephone": "+225-9552-254-977", + "pets": [ + "Toby", + "Murphy" + ], + "score": 5.9, + "email": "tatyana.hawthorne@yoga.com", + "url": "http://www.intimate.com", + "description": "violation lite summer visiting love priority mirrors refuse depression launched routes cable borders appreciation beverage hrs believes ni threat proper", + "verified": false, + "salary": 51426 + }, + { + "_id": "AAIH840SAXYFSUZX", + "name": "Martha Holloway", + "dob": "2016-06-28", + "address": { + "street": "2757 Wheatley Circle", + "town": "Wirksworth", + "postode": "BT3 9XJ" + }, + "telephone": "+218-8275-835-792", + "pets": [ + "Ginger", + "Ruby" + ], + "score": 8.3, + "email": "velva5437@yahoo.com", + "url": "https://www.dee.com", + "description": "reproductive disaster publishing source gamecube relocation hong climb db weblog mine taught norfolk toe routes assault casey blink direct circular", + "verified": false, + "salary": 39329 + }, + { + "_id": "6KYEPTV9M2KYGSHH", + "name": "Mireya Carden", + "dob": "2018-01-03", + "address": { + "street": "7539 Walmsley Avenue", + "town": "Kirkwall", + "postode": "BB7 8OG" + }, + "telephone": "+213-5408-612-852", + "pets": [ + "Gizmo", + "Riley" + ], + "score": 3, + "email": "kristine_hoskins37482@gmail.com", + "url": "http://www.supply.com", + "description": "sticks lesser lat sharon safe differently misc glenn forbidden listings schema unavailable slovak generating pharmacies packet tent practices governmental avg", + "verified": false, + "salary": 29502 + }, + { + "_id": "LNVQMDCMSQLSX9D2", + "name": "Angeline Chow-Hutchins", + "dob": "2014-07-06", + "address": { + "street": "0932 Haslam Avenue", + "town": "Cuckfield", + "postode": "M7 2JV" + }, + "telephone": "+64-2341-241-263", + "pets": [ + "Scooter", + "Lilly" + ], + "score": 3.1, + "email": "joy3181@slight.com", + "url": "http://www.teaching.com", + "description": "cardiff deputy abandoned minor mug mission demand rhythm celebs island larger informed likelihood egypt eminem passage bang park par sizes", + "verified": false, + "salary": 58720 + }, + { + "_id": "Q1SQ5RKZFA713LU6", + "name": "Fern Hildebrand", + "dob": "2015-08-09", + "address": { + "street": "3049 Cudworth", + "town": "Fairfield", + "postode": "N34 5TJ" + }, + "telephone": "+598-0743-342-958", + "pets": [ + "Patches", + "Bailey" + ], + "score": 6.8, + "email": "adeline.salerno@hotmail.com", + "url": "http://dump.com", + "description": "rainbow conditional managers karl entry buck theaters huge ko excluded ross mpeg torture attractions americas cf tags hairy expert spine", + "verified": false, + "salary": 18501 + }, + { + "_id": "7VY3LDI1X34UL4BL", + "name": "Antonetta Hoppe", + "dob": "2021-11-18", + "address": { + "street": "2542 Barrow Lane", + "town": "Minehead", + "postode": "N6 2VC" + }, + "telephone": "+241-8950-956-345", + "pets": [ + "Garfield", + "Murphy" + ], + "score": 4.9, + "email": "bella-carney@meat.com", + "url": "http://promoting.com", + "description": "catch blues savage agreements villages motorola regulations followed penalties motel demonstrates silk hang mpg monsters element flows bills render offshore", + "verified": false, + "salary": 49645 + }, + { + "_id": "6TOP00JX1UF5BSQG", + "name": "Lida Storm", + "dob": "2019-01-24", + "address": { + "street": "5866 Oliver Street", + "town": "Hythe", + "postode": "HS72 3LV" + }, + "telephone": "+213-8804-176-192", + "pets": [ + "Rocky", + "Max" + ], + "score": 8, + "email": "lelah_yamamoto@packing.com", + "url": "http://www.must.com", + "description": "occupied sales musicians calculation formed publication documented naval story africa recreation focal courage persian camel countries synthesis naturally became raising", + "verified": true, + "salary": 53855 + }, + { + "_id": "J2HBAGOMY0F0UG3Q", + "name": "Tammi Hoyle", + "dob": "2018-02-09", + "address": { + "street": "6495 Crossefield Circle", + "town": "Loddon", + "postode": "CV80 1UX" + }, + "telephone": "+61-8774-041-849", + "pets": [ + "Pepper", + "Penny" + ], + "score": 9.3, + "email": "cortezkearney93@yahoo.com", + "url": "http://payment.com", + "description": "debian eco andrews med sys licenses decreased infants vacuum adidas finds allowing td diff letting charity fixtures name attachment intervention", + "verified": true, + "salary": 47522 + }, + { + "_id": "MFIVKBQX1FI6J2VA", + "name": "Gabriela Stock", + "dob": "2016-09-04", + "address": { + "street": "8938 Butterhouse Avenue", + "town": "Edgware", + "postode": "TQ3 4MC" + }, + "telephone": "+599-6789-095-745", + "pets": [ + "Sasha", + "Lilly" + ], + "score": 5.8, + "email": "trudie5548@hotmail.com", + "url": "http://maternity.lib.mi.us", + "description": "transcription memorabilia court commodity online oem church restructuring lance material jet elections url autos wooden legislature pen mon controversial studies", + "verified": true, + "salary": 36784 + }, + { + "_id": "SQHEJ1UJN9S2OG3B", + "name": "Ferne Dodge", + "dob": "2014-09-16", + "address": { + "street": "8428 Squire Road", + "town": "Rowley Regis", + "postode": "KW1 5DN" + }, + "telephone": "+264-0561-357-213", + "pets": [ + "CoCo", + "Cooper" + ], + "score": 1.2, + "email": "giamartz@accurate.com", + "url": "https://www.hardware.tama.tokyo.jp", + "description": "fallen foot moral declaration plans submitting drivers rule samoa duplicate xi wants zone cedar facts contemporary drivers payments height known", + "verified": true, + "salary": 19567 + }, + { + "_id": "PEQZ5XCK1N1G8GSM", + "name": "Marisol Shipp", + "dob": "2020-03-08", + "address": { + "street": "6879 Huntley", + "town": "North Petherton", + "postode": "KT7 4ED" + }, + "telephone": "+507-4580-197-201", + "pets": [ + "Izzy", + "Charlie" + ], + "score": 10, + "email": "edithneumann@jones.com", + "url": "http://genome.dental", + "description": "sweden jose mastercard presented afternoon targeted horizon stroke recover springs diet glory libs choosing pediatric realtors justice current laos corps", + "verified": true, + "salary": 12524 + }, + { + "_id": "8BC65A8VESNQMQTK", + "name": "Connie Barr", + "dob": "2017-11-08", + "address": { + "street": "4381 Chestnut Avenue", + "town": "Mossley", + "postode": "LL78 7BV" + }, + "telephone": "+965-6813-369-079", + "pets": [ + "Daisy", + "Apollo" + ], + "score": 2.4, + "email": "santos396@equity.com", + "url": "https://www.jun.com", + "description": "coated disabled jungle addressing hoped airports collector importantly perfectly libraries hereby reseller dressing reasons tracker mountain procedure fusion lease patterns", + "verified": true, + "salary": 54612 + }, + { + "_id": "1NQYQR87LIQ77LO5", + "name": "Deshawn Rossi", + "dob": "2017-10-16", + "address": { + "street": "1017 Johnson", + "town": "Leicester", + "postode": "NP6 0CA" + }, + "telephone": "+596-5479-495-356", + "pets": [ + "Fiona", + "Henry" + ], + "score": 7, + "email": "louriescott@moral.com", + "url": "http://www.newspapers.com", + "description": "man lg neither item requires experiences read outstanding evolution bunny dialog licensing donation belongs multiple available product photo bb austin", + "verified": false, + "salary": 55744 + }, + { + "_id": "8F665QJU855HT5GA", + "name": "Elsy Hastings", + "dob": "2022-01-12", + "address": { + "street": "0059 Goodlad Road", + "town": "Kidderminster", + "postode": "AL8 7MT" + }, + "telephone": "+263-6677-286-946", + "pets": [ + "Harley", + "Jack" + ], + "score": 6.7, + "email": "winnifred14709@gmail.com", + "url": "http://dan.com", + "description": "yellow horizontal introduced w these dog forth culture doctor stuff prerequisite retrieval instructors pre publications looking dale auditor estonia accreditation", + "verified": false, + "salary": 33446 + }, + { + "_id": "A046T29QUGG7QJUL", + "name": "Wes Beauregard", + "dob": "2020-10-31", + "address": { + "street": "9046 Shiredale", + "town": "Burry Port", + "postode": "DH97 2SD" + }, + "telephone": "+263-2132-162-997", + "pets": [ + "Lucky", + "Rosie" + ], + "score": 9.5, + "email": "blanca-hastings-oneil@gmail.com", + "url": "https://insider.home-webserver.de", + "description": "authorized crime between treating fifteen mind physicians enhancement scenes formal booking skirt loop rapid well bidder buildings sao minimize cruise", + "verified": true, + "salary": 30363 + }, + { + "_id": "YEPM8BC07Q2KVYRA", + "name": "Valene Donald", + "dob": "2018-03-26", + "address": { + "street": "5031 Ashgate Avenue", + "town": "Manchester", + "postode": "RH85 7EZ" + }, + "telephone": "+43-7106-057-452", + "pets": [ + "Oreo", + "Scout" + ], + "score": 2.5, + "email": "valencia632@yahoo.com", + "url": "http://refugees.from-mi.com", + "description": "fig addition program concerning frankfurt premiere the sunrise canberra physician entertaining businesses vol fast attract assured guidance vaccine plots newman", + "verified": true, + "salary": 41638 + }, + { + "_id": "PDAQ4XLITD9O5U7M", + "name": "Ellamae Ferraro", + "dob": "2020-09-07", + "address": { + "street": "1434 Bridge Street", + "town": "Larbert", + "postode": "BD2 4LZ" + }, + "telephone": "+352-3112-572-537", + "pets": [ + "Cleo", + "Lucky" + ], + "score": 3.6, + "email": "junior_stephens@hotmail.com", + "url": "https://feelings.com", + "description": "defects kingdom hwy liechtenstein january latvia value watershed generator kansas hungry disease hh forbes documentation biggest longer productions belfast manager", + "verified": true, + "salary": 38826 + }, + { + "_id": "1JTLBKHUUGXQAMB5", + "name": "Ericka Winston", + "dob": "2019-06-05", + "address": { + "street": "2436 Haven", + "town": "Hereford", + "postode": "OL7 8DP" + }, + "telephone": "+92-4589-055-465", + "pets": [ + "Lily", + "Ellie" + ], + "score": 6.6, + "email": "mi.bruns1@gmail.com", + "url": "http://www.hockey.com", + "description": "sans charitable katrina factory pubmed tuesday todd dad carnival translations deeply vice political macintosh hardly affiliate respective issues lowest introducing", + "verified": true, + "salary": 62691 + }, + { + "_id": "MCSKZLECUEA2C5PU", + "name": "Jennie Wild", + "dob": "2021-09-27", + "address": { + "street": "5855 Moss Lane", + "town": "Erith", + "postode": "G39 5TB" + }, + "telephone": "+886-2362-259-199", + "pets": [ + "Zoe", + "Dexter" + ], + "score": 9.3, + "email": "darby-dominquez54107@yahoo.com", + "url": "http://trials.com", + "description": "henry reprints patrol leasing quad army ins sat latex realm intranet netherlands michel colors great wives restaurants watershed nato america", + "verified": true, + "salary": 58011 + }, + { + "_id": "2YQXFPO9CNZ1OIM0", + "name": "Luciano Bell", + "dob": "2017-01-28", + "address": { + "street": "5814 Badminton Avenue", + "town": "Driffield", + "postode": "GU39 2GZ" + }, + "telephone": "+237-0769-305-664", + "pets": [ + "Oscar", + "Marley" + ], + "score": 9.2, + "email": "raleigh.santoro@gmail.com", + "url": "https://www.laden.com", + "description": "charging inch annotated strings unwrap commission bank protected luxury label reef reserve belize devoted forecast gaming similar tool assistance gays", + "verified": true, + "salary": 37970 + }, + { + "_id": "26F8PT8LYUB2I1JX", + "name": "Harland Burrow", + "dob": "2022-11-11", + "address": { + "street": "2406 Levedale Circle", + "town": "Jarrow", + "postode": "CH94 2IY" + }, + "telephone": "+264-9589-530-892", + "pets": [ + "Boots", + "Duke" + ], + "score": 9.2, + "email": "saundra.dickinson45808@hotmail.com", + "url": "http://conservative.com", + "description": "nz athletes failing greece her thomson keeping quantities fc pet ships biblical sm necessary nursery spell injection way solution governmental", + "verified": true, + "salary": 53227 + }, + { + "_id": "M4MPZ8QTILYZEPVG", + "name": "Micha Moon", + "dob": "2015-04-26", + "address": { + "street": "2625 Brecon", + "town": "Aviemore", + "postode": "WS2 5WZ" + }, + "telephone": "+968-3199-782-340", + "pets": [ + "Fiona", + "Riley" + ], + "score": 7.4, + "email": "minta94202@sheriff.tc", + "url": "https://hobbies.com", + "description": "fibre columbia di informed reef indication lip component theatre philadelphia las attention comm received composition nov pirates mercy houston pine", + "verified": false, + "salary": 45216 + }, + { + "_id": "YXSG4RLXKEQ4EQ23", + "name": "Misha Hargis-Badger", + "dob": "2015-10-07", + "address": { + "street": "9782 Bonville Circle", + "town": "Cockermouth", + "postode": "SL5 7DG" + }, + "telephone": "+60-0104-691-642", + "pets": [ + "Sammy", + "Duke" + ], + "score": 6.1, + "email": "gussiegulley36@photographer.com", + "url": "https://supplier.com", + "description": "arrives subscriber retained obvious crystal sol remote logitech angel toe consistent pants membership popular sql disappointed survey msn incorporated beautiful", + "verified": true, + "salary": 30320 + }, + { + "_id": "CF1PCYPO65ABC8S1", + "name": "Usha Hutton", + "dob": "2017-05-05", + "address": { + "street": "8244 Thornden Circle", + "town": "Maldon", + "postode": "HD20 3VS" + }, + "telephone": "+49-4694-546-795", + "pets": [ + "bailey", + "Shadow" + ], + "score": 2.2, + "email": "marita-kauffman@upgrades.com", + "url": "http://www.charge.com", + "description": "medium harper climbing suse patients kelly doc cj eating telecom lost soil restoration wellington vault muscles determined promotional managing painful", + "verified": false, + "salary": 32091 + }, + { + "_id": "KHSQBLDRJS29FFNQ", + "name": "Sharen Mahon", + "dob": "2019-06-22", + "address": { + "street": "3572 Roosevelt Circle", + "town": "Southsea", + "postode": "MK40 8CC" + }, + "telephone": "+509-0783-559-952", + "pets": [ + "MIMI", + "Apollo" + ], + "score": 2.9, + "email": "cassaundra.ogden@experiments.com", + "url": "https://exciting.com", + "description": "avon bookmark guyana correction colored concerts medline fence things fun comments aims behavior concerts wood particles structures drawn late eur", + "verified": true, + "salary": 40932 + }, + { + "_id": "3BFQ98IC0N611M8Y", + "name": "Myrna Block", + "dob": "2015-10-10", + "address": { + "street": "9756 Downton Avenue", + "town": "Tranent", + "postode": "CH8 3PK" + }, + "telephone": "+27-2834-868-866", + "pets": [ + "Kitty", + "Marley" + ], + "score": 8.3, + "email": "whitleyspangler@hotmail.com", + "url": "https://www.saturn.com", + "description": "few restructuring vsnet yea forgot gmt afterwards township digest nn might acrobat engineering co gnu indoor backup oem corner accomplish", + "verified": true, + "salary": 37936 + }, + { + "_id": "0AH1BEXYO0NKYRCM", + "name": "Brant Ogden", + "dob": "2021-09-01", + "address": { + "street": "9897 Midhurst Lane", + "town": "Portishead", + "postode": "BL8 5UO" + }, + "telephone": "+598-7536-682-209", + "pets": [ + "Cali", + "Buddy" + ], + "score": 3, + "email": "coralie_heilman@lands.com", + "url": "http://ping.com", + "description": "dakota kings antigua method approaches occupied filtering agreed elements generic publicly poverty lewis model ftp pakistan willow guards reel acts", + "verified": false, + "salary": 60547 + }, + { + "_id": "SMSNN0UK8EVID3OQ", + "name": "Alvaro Estes", + "dob": "2016-08-03", + "address": { + "street": "5187 Ashbourne Street", + "town": "New Quay", + "postode": "TA23 8IA" + }, + "telephone": "+968-6146-170-706", + "pets": [ + "Frankie", + "Henry" + ], + "score": 8.5, + "email": "gale-seifert50@corruption.com", + "url": "https://emotions.com", + "description": "loan displaying cause mon mainland seo characteristic agencies protection glen dedicated representative headline records disclaimers width lookup basketball liabilities wagon", + "verified": true, + "salary": 46244 + }, + { + "_id": "CEHLEBI1T64OSPPK", + "name": "Shawna Thiel", + "dob": "2017-02-24", + "address": { + "street": "8573 Gillingham Circle", + "town": "Stokesley", + "postode": "TW3 6YB" + }, + "telephone": "+241-0681-315-106", + "pets": [ + "Buddy", + "Duke" + ], + "score": 9.8, + "email": "glenniespencer41@hotmail.com", + "url": "http://www.message.com", + "description": "certification affect opposite wisconsin assembled ns inside magnificent received dave body roots assistant precision wire speak ram des diy studio", + "verified": true, + "salary": 49137 + }, + { + "_id": "ATTAEOSDEMJCDLDJ", + "name": "Kirk Cleveland", + "dob": "2019-11-29", + "address": { + "street": "6339 Artists", + "town": "Hockley", + "postode": "TN4 1KC" + }, + "telephone": "+687-1298-392-561", + "pets": [ + "Kitty", + "Cody" + ], + "score": 3.7, + "email": "dudleystuckey4538@ca.com", + "url": "http://www.ago.com", + "description": "rochester annex prototype lc giants purchasing beatles rpm submissions doctrine socks bachelor runtime passive s fifth ferrari fishing automated china", + "verified": true, + "salary": 60860 + }, + { + "_id": "QBN0GIBF2Y62L793", + "name": "Dorla Chapman", + "dob": "2018-07-23", + "address": { + "street": "9817 Mattison", + "town": "Farnborough", + "postode": "SM74 5EV" + }, + "telephone": "+971-8528-476-539", + "pets": [ + "Rusty", + "Max" + ], + "score": 3, + "email": "georgenecarrasco-mcnamee87@emerald.com", + "url": "https://precisely.com", + "description": "peaceful bargains archive suffered baths steady pop communicate entertainment glenn successfully system levy tire keeps developments joins battlefield rope weeks", + "verified": true, + "salary": 49009 + }, + { + "_id": "APUP4O5V6Y0L0FOM", + "name": "Lorretta Hutchins", + "dob": "2021-07-07", + "address": { + "street": "0581 Polefield Avenue", + "town": "Nantwich", + "postode": "KA22 1AT" + }, + "telephone": "+592-4970-580-423", + "pets": [ + "Phoebe", + "Duke" + ], + "score": 8.8, + "email": "odeliadavenport@hotmail.com", + "url": "https://extending.com", + "description": "requests tracy exchange inquiries duo importance collector including modify norway rack insulin boots physician threatened bali permits sussex fonts presents", + "verified": true, + "salary": 27235 + }, + { + "_id": "A0KAFOLKOC32UD31", + "name": "Jo Shields", + "dob": "2014-06-28", + "address": { + "street": "0060 Hus", + "town": "Insch", + "postode": "SR09 6FM" + }, + "telephone": "+91-0751-361-536", + "pets": [ + "Rocky", + "Sasha" + ], + "score": 4, + "email": "syreeta5@flour.com", + "url": "http://www.daily.com", + "description": "westminster media prints belfast distinction theater improved customize via immediate sympathy post notices enormous decision forget factor entertaining tool simultaneously", + "verified": true, + "salary": 57058 + }, + { + "_id": "01BX108KTUF9QQ7E", + "name": "Branden Galloway", + "dob": "2018-09-24", + "address": { + "street": "6815 Wallingford Road", + "town": "Burntisland", + "postode": "BT2 5BD" + }, + "telephone": "+852-6396-906-564", + "pets": [ + "Gracie", + "Duke" + ], + "score": 8.6, + "email": "lala_kessler73636@encourages.com", + "url": "http://arabic.com", + "description": "elevation id specialized smoking pmid fwd persons quantities console cancer boxing baby chem rule hong reviewer horse comfortable olive dh", + "verified": false, + "salary": 21792 + } + ] + } + """.trimIndent() From 42f0ce7a40318a685bf24c7eb5cfe33c018aafc8 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sat, 21 Feb 2026 16:34:17 +0100 Subject: [PATCH 08/32] Optimize diff generation and refactor UI --- .../jsontree/diff/JsonTreeDiff.kt | 180 ++++++------------ .../jsontree/diff/JsonTreeDiffColors.kt | 4 + .../jsontree/diff/JsonTreeDiffer.kt | 155 +++++++-------- .../jsontree/diff/JsonTreeDifferState.kt | 4 +- sample/src/commonMain/kotlin/App.kt | 5 +- 5 files changed, 149 insertions(+), 199 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index 0c5c888..ccaf2e5 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -2,9 +2,12 @@ package com.sebastianneubauer.jsontree.diff import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -21,6 +24,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.sebastianneubauer.jsontree.CollapsableType @@ -41,68 +45,6 @@ public fun JsonTreeDiff( textStyle: TextStyle = LocalTextStyle.current, onError: (JsonTreeDiffError) -> Unit = {} ) { -// val original = """ -// { -// "topLevelObject": { -// "string": "stringValue", -// "nestedObject": { -// "int": 42, -// "nestedArray": [ -// "nestedArrayValue", -// "nestedArrayValue" -// ], -// "arrayOfObjects": [ -// { -// "anotherString": "anotherStringValue" -// }, -// { -// "anotherInt": 52 -// } -// ] -// } -// }, -// "topLevelArray": [ -// "hello", -// "world" -// ], -// "emptyObject": { -// -// } -// } -// """.trimIndent() -// -// val revised = """ -// { -// "topLevelObject": { -// "string": "stringValue", -// "nestedObject": { -// "nestedArray": [ -// "nestedArrayValue", -// "nestedArrayValue" -// ], -// "rrayOfa": [ -// { -// "anotherString": "anotherStringValue" -// }, -// { -// "anotherInt": 52 -// }, -// { -// "anotherFloat": 5.0 -// } -// ] -// } -// }, -// "topLevelArray": [ -// "hello", -// "world" -// ], -// "emptyObject": { -// -// } -// } -// """.trimIndent() - val jsonTreeDiffer = remember(originalJson, revisedJson) { JsonTreeDiffer( defaultDispatcher = Dispatchers.Default, @@ -147,80 +89,82 @@ private fun SideBySideDiff( colors: JsonTreeDiffColors, textStyle: TextStyle, ) { - Row(modifier = Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier.weight(1f), - state = originalListState - ) { - itemsIndexed(state.originalJsonDiffElements) { index, diffElement -> - val jsonTreeElement = when(diffElement) { - is JsonDiffElement.Change -> diffElement.jsonTreeElement - is JsonDiffElement.Deletion -> diffElement.jsonTreeElement!! - is JsonDiffElement.Equal -> diffElement.jsonTreeElement - is JsonDiffElement.Insertion -> null - } + LazyColumn( + modifier = Modifier.fillMaxWidth(), + state = originalListState + ) { + itemsIndexed(state.diffElements) { index, (originalDiffElement, revisedDiffElement) -> + val originalJsonTreeElement = when(originalDiffElement) { + is JsonDiffElement.Change -> originalDiffElement.jsonTreeElement + is JsonDiffElement.Deletion -> originalDiffElement.jsonTreeElement!! + is JsonDiffElement.Equal -> originalDiffElement.jsonTreeElement + is JsonDiffElement.Insertion -> null + } + val originalText = rememberText( + jsonTreeElement = originalJsonTreeElement, + diffIndices = if(originalDiffElement is JsonDiffElement.Change) { + originalDiffElement.inlineDiffIndices + } else null, + colors = colors, + highlightColor = colors.deletionHighlightColor, + ) - val text = rememberText( - jsonTreeElement = jsonTreeElement, - diffIndices = if(diffElement is JsonDiffElement.Change) { - diffElement.inlineDiffIndices - } else null, - colors = colors, - highlightColor = colors.deletionHighlightColor, - ) + val revisedJsonTreeElement = when(revisedDiffElement) { + is JsonDiffElement.Change -> revisedDiffElement.jsonTreeElement + is JsonDiffElement.Deletion -> null + is JsonDiffElement.Equal -> revisedDiffElement.jsonTreeElement + is JsonDiffElement.Insertion -> revisedDiffElement.jsonTreeElement!! + } + val revisedText = rememberText( + jsonTreeElement = revisedJsonTreeElement, + diffIndices = if(revisedDiffElement is JsonDiffElement.Change) { + revisedDiffElement.inlineDiffIndices + } else null, + colors = colors, + highlightColor = colors.insertionHighlightColor + ) + Row( + modifier = Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth() + ) { DiffText( - backgroundColor = when(diffElement) { + modifier = Modifier + .weight(1F) + .fillMaxHeight(), + backgroundColor = when(originalDiffElement) { is JsonDiffElement.Change -> colors.deletionBackgroundColor is JsonDiffElement.Deletion -> colors.deletionBackgroundColor is JsonDiffElement.Equal -> Color.Transparent - is JsonDiffElement.Insertion -> Color.Transparent + is JsonDiffElement.Insertion -> colors.changeBackgroundColor }, - indent = if(jsonTreeElement != null && index > 0) { - 20.dp * jsonTreeElement.level + indent = if(originalJsonTreeElement != null && index > 0) { + 20.dp * originalJsonTreeElement.level } else { 0.dp }, textStyle = textStyle, - text = text, - ) - } - } - - LazyColumn( - modifier = Modifier.weight(1f), - state = revisedListState - ) { - itemsIndexed(state.revisedJsonDiffElements) { index, diffElement -> - val jsonTreeElement = when(diffElement) { - is JsonDiffElement.Change -> diffElement.jsonTreeElement - is JsonDiffElement.Deletion -> null - is JsonDiffElement.Equal -> diffElement.jsonTreeElement - is JsonDiffElement.Insertion -> diffElement.jsonTreeElement!! - } - val text = rememberText( - jsonTreeElement = jsonTreeElement, - diffIndices = if(diffElement is JsonDiffElement.Change) { - diffElement.inlineDiffIndices - } else null, - colors = colors, - highlightColor = colors.insertionHighlightColor + text = originalText, ) DiffText( - backgroundColor = when(diffElement) { + modifier = Modifier + .weight(1F) + .fillMaxHeight(), + backgroundColor = when(revisedDiffElement) { is JsonDiffElement.Change -> colors.insertionBackgroundColor - is JsonDiffElement.Deletion -> Color.Transparent + is JsonDiffElement.Deletion -> colors.changeBackgroundColor is JsonDiffElement.Equal -> Color.Transparent is JsonDiffElement.Insertion -> colors.insertionBackgroundColor }, - indent = if(jsonTreeElement != null && index > 0) { - 20.dp * jsonTreeElement.level + indent = if(revisedJsonTreeElement != null && index > 0) { + 20.dp * revisedJsonTreeElement.level } else { 0.dp }, textStyle = textStyle, - text = text + text = revisedText ) } } @@ -229,18 +173,18 @@ private fun SideBySideDiff( @Composable private fun DiffText( + modifier: Modifier, backgroundColor: Color, indent: Dp, textStyle: TextStyle, text: AnnotatedString, ) { Text( - modifier = Modifier - .fillMaxWidth() + modifier = modifier .background(color = backgroundColor) .padding(start = indent), style = textStyle, - text = text + text = text, ) } @@ -304,12 +248,10 @@ private fun SyncScrollingEffect( } } - // Observe scroll changes in the LEFT column LaunchedEffect(originalListState.firstVisibleItemIndex, originalListState.firstVisibleItemScrollOffset) { syncScroll(originalListState, revisedListState) } - // Observe scroll changes in the RIGHT column LaunchedEffect(revisedListState.firstVisibleItemIndex, revisedListState.firstVisibleItemScrollOffset) { syncScroll(revisedListState, originalListState) } diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt index 843e35a..eebf926 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color * @param deletionBackgroundColor The background color for lines that contain deletion diffs. * @param insertionHighlightColor TThe color for highlighting insertions inside a line. * @param insertionBackgroundColor The background color for lines that contain insertion diffs. + * @param changeBackgroundColor The background color for lines that have changes on the other side of the diff. */ public data class JsonTreeDiffColors( val keyColor: Color, @@ -27,6 +28,7 @@ public data class JsonTreeDiffColors( val deletionBackgroundColor: Color, val insertionHighlightColor: Color, val insertionBackgroundColor: Color, + val changeBackgroundColor: Color ) /** @@ -43,6 +45,7 @@ public val defaultLightDiffColors: JsonTreeDiffColors = JsonTreeDiffColors( deletionBackgroundColor = Color(0xFFFFEBE9), insertionHighlightColor = Color(0xFFACEEBB), insertionBackgroundColor = Color(0xFFDBFBE1), + changeBackgroundColor = Color(0xFFF7F8FA), ) /** @@ -59,4 +62,5 @@ public val defaultDarkDiffColors: JsonTreeDiffColors = JsonTreeDiffColors( deletionBackgroundColor = Color(0xFF352D33), insertionHighlightColor = Color(0xFF345E3D), insertionBackgroundColor = Color(0xFF263834), + changeBackgroundColor = Color(0xFF262C36), ) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index cba7808..ef99871 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -56,6 +56,7 @@ internal class JsonTreeDiffer( } val diffRows = DiffRowGenerator( + columnWidth = 0, // Needs to be 0, otherwise HTML linebreaks will be inserted in diff lines. showInlineDiffs = true, newTag = diffRowGenerator, oldTag = diffRowGenerator, @@ -64,119 +65,119 @@ internal class JsonTreeDiffer( revisedJsonTreeList.map { it.toRenderString() } ) - val inlineDiffsIndices = diffRows.map { diffRow -> - if(diffRow.tag == DiffRow.Tag.CHANGE) { + val usedOriginalIds = mutableListOf() + val usedRevisedIds = mutableListOf() + + val diffElements = diffRows.map { diffRow -> + println("DiffRow: ${diffRow.tag}: ${diffRow.oldLine} -> ${diffRow.newLine}") + + val inlineDiffsIndices = if(diffRow.tag == DiffRow.Tag.CHANGE) { val oldLineIndices = diffRow.oldLine.findInlineDiffTagIndices() val newLineIndices = diffRow.newLine.findInlineDiffTagIndices() Pair(oldLineIndices, newLineIndices) } else { Pair(emptyList(), emptyList()) } - } - val strippedTagDiffRows = diffRows.map { diffRow -> - diffRow.copy( + val strippedTagDiffRow = diffRow.copy( oldLine = diffRow.oldLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, ""), newLine = diffRow.newLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, "") ) - } + println("Stripped: ${strippedTagDiffRow.tag}: ${strippedTagDiffRow.oldLine} -> ${strippedTagDiffRow.newLine}") - val originalDiffElements = getOriginalDiffElements( - strippedTagDiffRows = strippedTagDiffRows, - originalJsonTreeList = originalJsonTreeList, - inlineDiffsIndices = inlineDiffsIndices - ) + val originalDiffElement = getOriginalDiffElement( + diffRow = strippedTagDiffRow, + usedIds = usedOriginalIds, + originalJsonTreeList = originalJsonTreeList, + inlineDiffsIndices = inlineDiffsIndices.first + ) - val revisedDiffElements = getRevisedDiffElements( - strippedTagDiffRows = strippedTagDiffRows, - revisedJsonTreeList = revisedJsonTreeList, - inlineDiffsIndices = inlineDiffsIndices - ) + val revisedDiffElement = getRevisedDiffElement( + diffRow = strippedTagDiffRow, + usedIds = usedRevisedIds, + revisedJsonTreeList = revisedJsonTreeList, + inlineDiffsIndices = inlineDiffsIndices.second + ) + + Pair(originalDiffElement, revisedDiffElement) + } withContext(mainDispatcher) { state.value = JsonTreeDifferState.Ready( - originalJsonDiffElements = originalDiffElements, - revisedJsonDiffElements = revisedDiffElements, + diffElements = diffElements ) } } - private fun getOriginalDiffElements( - strippedTagDiffRows: List, + private fun getOriginalDiffElement( + diffRow: DiffRow, + usedIds: MutableList, originalJsonTreeList: List, - inlineDiffsIndices: List>, List>>> - ): List { - val usedIds = mutableListOf() - + inlineDiffsIndices: List> + ): JsonDiffElement { fun findJsonTreeElement(diffLine: String): JsonTreeElement { return originalJsonTreeList.first { jsonTreeElement -> diffLine == jsonTreeElement.toRenderString() && jsonTreeElement.id !in usedIds } } - return strippedTagDiffRows.mapIndexed { index, diffRow -> - println("Diff: Index: $index, $diffRow") - when(diffRow.tag) { - DiffRow.Tag.EQUAL -> { - val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) - usedIds.add(jsonTreeElement.id) - JsonDiffElement.Equal(jsonTreeElement) - } - DiffRow.Tag.CHANGE -> { - val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) - usedIds.add(jsonTreeElement.id) - JsonDiffElement.Change( - jsonTreeElement = jsonTreeElement, - inlineDiffIndices = inlineDiffsIndices[index].first - ) - } - DiffRow.Tag.INSERT -> { - JsonDiffElement.Insertion(null) - } - DiffRow.Tag.DELETE -> { - val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) - usedIds.add(jsonTreeElement.id) - JsonDiffElement.Deletion(jsonTreeElement) - } + return when(diffRow.tag) { + DiffRow.Tag.EQUAL -> { + val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) + usedIds.add(jsonTreeElement.id) + JsonDiffElement.Equal(jsonTreeElement) + } + DiffRow.Tag.CHANGE -> { + val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) + usedIds.add(jsonTreeElement.id) + JsonDiffElement.Change( + jsonTreeElement = jsonTreeElement, + inlineDiffIndices = inlineDiffsIndices + ) + } + DiffRow.Tag.INSERT -> { + JsonDiffElement.Insertion(null) + } + DiffRow.Tag.DELETE -> { + val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) + usedIds.add(jsonTreeElement.id) + JsonDiffElement.Deletion(jsonTreeElement) } } } - private fun getRevisedDiffElements( - strippedTagDiffRows: List, + private fun getRevisedDiffElement( + diffRow: DiffRow, + usedIds: MutableList, revisedJsonTreeList: List, - inlineDiffsIndices: List>, List>>> - ): List { - val usedIds = mutableListOf() - + inlineDiffsIndices: List> + ): JsonDiffElement { fun findJsonTreeElement(diffLine: String): JsonTreeElement { return revisedJsonTreeList.first { jsonTreeElement -> diffLine == jsonTreeElement.toRenderString() && jsonTreeElement.id !in usedIds } } - return strippedTagDiffRows.mapIndexed { index, diffRow -> - when(diffRow.tag) { - DiffRow.Tag.EQUAL -> { - val jsonTreeElement = findJsonTreeElement(diffRow.newLine) - usedIds.add(jsonTreeElement.id) - JsonDiffElement.Equal(jsonTreeElement) - } - DiffRow.Tag.CHANGE -> { - val jsonTreeElement = findJsonTreeElement(diffRow.newLine) - usedIds.add(jsonTreeElement.id) - JsonDiffElement.Change( - jsonTreeElement = jsonTreeElement, - inlineDiffIndices = inlineDiffsIndices[index].second - ) - } - DiffRow.Tag.INSERT -> { - val jsonTreeElement = findJsonTreeElement(diffRow.newLine) - usedIds.add(jsonTreeElement.id) - JsonDiffElement.Insertion(jsonTreeElement) - } - DiffRow.Tag.DELETE -> JsonDiffElement.Deletion(null) + return when(diffRow.tag) { + DiffRow.Tag.EQUAL -> { + val jsonTreeElement = findJsonTreeElement(diffRow.newLine) + usedIds.add(jsonTreeElement.id) + JsonDiffElement.Equal(jsonTreeElement) + } + DiffRow.Tag.CHANGE -> { + val jsonTreeElement = findJsonTreeElement(diffRow.newLine) + usedIds.add(jsonTreeElement.id) + JsonDiffElement.Change( + jsonTreeElement = jsonTreeElement, + inlineDiffIndices = inlineDiffsIndices + ) + } + DiffRow.Tag.INSERT -> { + val jsonTreeElement = findJsonTreeElement(diffRow.newLine) + usedIds.add(jsonTreeElement.id) + JsonDiffElement.Insertion(jsonTreeElement) } + DiffRow.Tag.DELETE -> JsonDiffElement.Deletion(null) } } @@ -222,7 +223,7 @@ internal class JsonTreeDiffer( return result } - private val inlineDiffTagOpen = "!!$$$$!!" - private val inlineDiffTagClosed = "!!/$$$$!!" + private val inlineDiffTagOpen = "JSON_TREE_DIFF_START_TAG" + private val inlineDiffTagClosed = "JSON_TREE_DIFF_END_TAG" } diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt index a8bc011..c096efc 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt @@ -2,6 +2,7 @@ package com.sebastianneubauer.jsontree.diff import androidx.compose.runtime.Immutable import com.sebastianneubauer.jsontree.JsonTreeElement +import kotlinx.serialization.json.Json @Immutable internal sealed interface JsonTreeDifferState { @@ -9,8 +10,7 @@ internal sealed interface JsonTreeDifferState { data object Loading: JsonTreeDifferState data class Ready( - val originalJsonDiffElements: List, - val revisedJsonDiffElements: List + val diffElements: List> ): JsonTreeDifferState @Immutable diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt index cf9d69a..ddbf621 100644 --- a/sample/src/commonMain/kotlin/App.kt +++ b/sample/src/commonMain/kotlin/App.kt @@ -370,7 +370,10 @@ private fun JsonDiff( JsonTreeDiff( modifier = Modifier .fillMaxWidth() - .height(400.dp), + .height(400.dp) + .background( + if (colors == defaultLightColors) Color.Unspecified else Color.Black + ), originalJson = originalJson, revisedJson = complexJsonRevised, onLoading = { From 6735b856eee17e03ebe33dd40c9646c83d026637 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sun, 22 Feb 2026 15:23:36 +0100 Subject: [PATCH 09/32] Fix background color and recomposition --- .../jsontree/diff/JsonTreeDiff.kt | 34 ++++++++++++++----- sample/src/commonMain/kotlin/App.kt | 30 +++++++++------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index ccaf2e5..3dd98f5 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -2,6 +2,7 @@ package com.sebastianneubauer.jsontree.diff import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight @@ -51,12 +52,12 @@ public fun JsonTreeDiff( mainDispatcher = Dispatchers.Main ) } - val state = jsonTreeDiffer.state.collectAsState().value - LaunchedEffect(Unit) { + LaunchedEffect(jsonTreeDiffer) { jsonTreeDiffer.diff(originalJson, revisedJson) } + val state = jsonTreeDiffer.state.collectAsState().value val originalListState = rememberLazyListState() val revisedListState = rememberLazyListState() @@ -139,6 +140,7 @@ private fun SideBySideDiff( is JsonDiffElement.Equal -> Color.Transparent is JsonDiffElement.Insertion -> colors.changeBackgroundColor }, + backgroundFillColor = colors.changeBackgroundColor, indent = if(originalJsonTreeElement != null && index > 0) { 20.dp * originalJsonTreeElement.level } else { @@ -158,6 +160,7 @@ private fun SideBySideDiff( is JsonDiffElement.Equal -> Color.Transparent is JsonDiffElement.Insertion -> colors.insertionBackgroundColor }, + backgroundFillColor = colors.changeBackgroundColor, indent = if(revisedJsonTreeElement != null && index > 0) { 20.dp * revisedJsonTreeElement.level } else { @@ -175,17 +178,30 @@ private fun SideBySideDiff( private fun DiffText( modifier: Modifier, backgroundColor: Color, + backgroundFillColor: Color, indent: Dp, textStyle: TextStyle, text: AnnotatedString, ) { - Text( - modifier = modifier - .background(color = backgroundColor) - .padding(start = indent), - style = textStyle, - text = text, - ) + Column(modifier = modifier) { + Text( + modifier = Modifier + .fillMaxWidth() + .background(color = backgroundColor) + .padding(start = indent), + style = textStyle, + text = text, + ) + + // If the change on the other side spans multiple lines, + // fill the rest of this side with a background color + Box( + modifier = Modifier + .weight(1F) + .fillMaxWidth() + .background(color = backgroundFillColor) + ) + } } @Composable diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt index ddbf621..db3d82a 100644 --- a/sample/src/commonMain/kotlin/App.kt +++ b/sample/src/commonMain/kotlin/App.kt @@ -30,6 +30,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextField +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -83,9 +85,11 @@ private fun MainScreen() { style = MaterialTheme.typography.headlineMedium, ) }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.White), ) }, contentWindowInsets = WindowInsets(top = 60.dp), + containerColor = Color.White ) { padding -> Column( modifier = Modifier @@ -214,6 +218,15 @@ private fun MainScreen() { Spacer(Modifier.height(8.dp)) + if(showDiff) { + JsonDiff( + originalJson = json, + colors = colors, + ) + } + + Spacer(Modifier.height(8.dp)) + FlowRow(modifier = Modifier.padding(horizontal = 8.dp)) { TextField( value = searchQuery, @@ -254,15 +267,6 @@ private fun MainScreen() { Spacer(Modifier.height(8.dp)) - if(showDiff) { - JsonDiff( - originalJson = json, - colors = colors, - ) - } - - Spacer(Modifier.height(8.dp)) - val pagerState = rememberPagerState(initialPage = 0, pageCount = { 3 }) //Pager to test leaving composition @@ -353,7 +357,7 @@ private fun JsonDiff( originalJson: String, colors: TreeColors ) { - var error by remember { mutableStateOf(null) } + var error by remember(originalJson) { mutableStateOf(null) } val errorMessage = error if (errorMessage != null) { @@ -361,7 +365,7 @@ private fun JsonDiff( modifier = Modifier .fillMaxSize() .background( - color = if (colors == defaultLightColors) Color.Unspecified else Color.Black + color = if (colors == defaultLightColors) Color.White else Color.Black ), text = errorMessage, color = if (colors == defaultLightColors) Color.Black else Color.White, @@ -372,7 +376,7 @@ private fun JsonDiff( .fillMaxWidth() .height(400.dp) .background( - if (colors == defaultLightColors) Color.Unspecified else Color.Black + if (colors == defaultLightColors) Color.White else Color.Black ), originalJson = originalJson, revisedJson = complexJsonRevised, @@ -381,7 +385,7 @@ private fun JsonDiff( modifier = Modifier .fillMaxSize() .background( - if (colors == defaultLightColors) Color.Unspecified else Color.Black + if (colors == defaultLightColors) Color.White else Color.Black ), contentAlignment = Alignment.Center ) { From 392b2254e45d111005573c436b96b84af39c1fdd Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sun, 22 Feb 2026 15:32:27 +0100 Subject: [PATCH 10/32] Remove scroll syncing --- .../jsontree/diff/JsonTreeDiff.kt | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index 3dd98f5..4c93e77 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -57,22 +57,11 @@ public fun JsonTreeDiff( jsonTreeDiffer.diff(originalJson, revisedJson) } - val state = jsonTreeDiffer.state.collectAsState().value - val originalListState = rememberLazyListState() - val revisedListState = rememberLazyListState() - - SyncScrollingEffect( - originalListState = originalListState, - revisedListState = revisedListState, - ) - - when(state) { + when(val state = jsonTreeDiffer.state.collectAsState().value) { is JsonTreeDifferState.Loading -> onLoading() is JsonTreeDifferState.Ready -> Box(modifier = modifier) { SideBySideDiff( state = state, - originalListState = originalListState, - revisedListState = revisedListState, colors = colors, textStyle = textStyle, ) @@ -85,14 +74,11 @@ public fun JsonTreeDiff( @Composable private fun SideBySideDiff( state: JsonTreeDifferState.Ready, - originalListState: LazyListState, - revisedListState: LazyListState, colors: JsonTreeDiffColors, textStyle: TextStyle, ) { LazyColumn( modifier = Modifier.fillMaxWidth(), - state = originalListState ) { itemsIndexed(state.diffElements) { index, (originalDiffElement, revisedDiffElement) -> val originalJsonTreeElement = when(originalDiffElement) { @@ -246,29 +232,4 @@ private fun rememberText( ) null -> AnnotatedString("") } -} - -@Composable -private fun SyncScrollingEffect( - originalListState: LazyListState, - revisedListState: LazyListState, -) { - val coroutineScope = rememberCoroutineScope() - fun syncScroll(leading: LazyListState, following: LazyListState) { - coroutineScope.launch { - val scrollPosition = leading.firstVisibleItemScrollOffset - following.scrollToItem( - index = leading.firstVisibleItemIndex, - scrollOffset = scrollPosition - ) - } - } - - LaunchedEffect(originalListState.firstVisibleItemIndex, originalListState.firstVisibleItemScrollOffset) { - syncScroll(originalListState, revisedListState) - } - - LaunchedEffect(revisedListState.firstVisibleItemIndex, revisedListState.firstVisibleItemScrollOffset) { - syncScroll(revisedListState, originalListState) - } -} +} \ No newline at end of file From 9a950b67e60478922724452712381d462c960df3 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sun, 15 Mar 2026 13:19:26 +0000 Subject: [PATCH 11/32] Add regularBackgroundColor and showInlineDiffs param --- .../jsontree/diff/JsonTreeDiff.kt | 15 ++++++--- .../jsontree/diff/JsonTreeDiffColors.kt | 6 +++- .../jsontree/diff/JsonTreeDiffer.kt | 31 +++++++++++-------- ...nsions.kt => JsonTreeElementExtensions.kt} | 0 ...st.kt => JsonTreeElementExtensionsTest.kt} | 2 +- sample/src/commonMain/kotlin/App.kt | 11 +++++++ 6 files changed, 45 insertions(+), 20 deletions(-) rename jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/{Extensions.kt => JsonTreeElementExtensions.kt} (100%) rename jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/{ExtensionsTest.kt => JsonTreeElementExtensionsTest.kt} (99%) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index 4c93e77..ca16737 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -42,19 +42,24 @@ public fun JsonTreeDiff( revisedJson: String, onLoading: @Composable () -> Unit, modifier: Modifier = Modifier, + showInlineDiffs: Boolean = true, colors: JsonTreeDiffColors = defaultLightDiffColors, textStyle: TextStyle = LocalTextStyle.current, onError: (JsonTreeDiffError) -> Unit = {} ) { - val jsonTreeDiffer = remember(originalJson, revisedJson) { + val jsonTreeDiffer = remember { JsonTreeDiffer( defaultDispatcher = Dispatchers.Default, mainDispatcher = Dispatchers.Main ) } - LaunchedEffect(jsonTreeDiffer) { - jsonTreeDiffer.diff(originalJson, revisedJson) + LaunchedEffect(originalJson, revisedJson, showInlineDiffs) { + jsonTreeDiffer.diff( + original = originalJson, + revised = revisedJson, + showInlineDiffs = showInlineDiffs + ) } when(val state = jsonTreeDiffer.state.collectAsState().value) { @@ -123,7 +128,7 @@ private fun SideBySideDiff( backgroundColor = when(originalDiffElement) { is JsonDiffElement.Change -> colors.deletionBackgroundColor is JsonDiffElement.Deletion -> colors.deletionBackgroundColor - is JsonDiffElement.Equal -> Color.Transparent + is JsonDiffElement.Equal -> colors.regularBackgroundColor is JsonDiffElement.Insertion -> colors.changeBackgroundColor }, backgroundFillColor = colors.changeBackgroundColor, @@ -143,7 +148,7 @@ private fun SideBySideDiff( backgroundColor = when(revisedDiffElement) { is JsonDiffElement.Change -> colors.insertionBackgroundColor is JsonDiffElement.Deletion -> colors.changeBackgroundColor - is JsonDiffElement.Equal -> Color.Transparent + is JsonDiffElement.Equal -> colors.regularBackgroundColor is JsonDiffElement.Insertion -> colors.insertionBackgroundColor }, backgroundFillColor = colors.changeBackgroundColor, diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt index eebf926..d35439e 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.Color * @param insertionHighlightColor TThe color for highlighting insertions inside a line. * @param insertionBackgroundColor The background color for lines that contain insertion diffs. * @param changeBackgroundColor The background color for lines that have changes on the other side of the diff. + * @param regularBackgroundColor The background color for lines that are equal on both sides of the diff. */ public data class JsonTreeDiffColors( val keyColor: Color, @@ -28,7 +29,8 @@ public data class JsonTreeDiffColors( val deletionBackgroundColor: Color, val insertionHighlightColor: Color, val insertionBackgroundColor: Color, - val changeBackgroundColor: Color + val changeBackgroundColor: Color, + val regularBackgroundColor: Color, ) /** @@ -46,6 +48,7 @@ public val defaultLightDiffColors: JsonTreeDiffColors = JsonTreeDiffColors( insertionHighlightColor = Color(0xFFACEEBB), insertionBackgroundColor = Color(0xFFDBFBE1), changeBackgroundColor = Color(0xFFF7F8FA), + regularBackgroundColor = Color(0xFFFFFFFF) ) /** @@ -63,4 +66,5 @@ public val defaultDarkDiffColors: JsonTreeDiffColors = JsonTreeDiffColors( insertionHighlightColor = Color(0xFF345E3D), insertionBackgroundColor = Color(0xFF263834), changeBackgroundColor = Color(0xFF262C36), + regularBackgroundColor = Color(0xFF0C1117) ) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index ef99871..50351cd 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -6,6 +6,7 @@ import com.sebastianneubauer.jsontree.JsonTreeParserState import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement import com.sebastianneubauer.jsontree.util.toRenderString +import io.github.petertrr.diffutils.text.DiffLineNormalizer import io.github.petertrr.diffutils.text.DiffRow import io.github.petertrr.diffutils.text.DiffRowGenerator import io.github.petertrr.diffutils.text.DiffTagGenerator @@ -19,13 +20,19 @@ internal class JsonTreeDiffer( val defaultDispatcher: CoroutineDispatcher, val mainDispatcher: CoroutineDispatcher ) { - val state = MutableStateFlow(JsonTreeDifferState.Loading) suspend fun diff( original: String, revised: String, + showInlineDiffs: Boolean, ) = withContext(defaultDispatcher) { + if(state.value !is JsonTreeDifferState.Loading) { + withContext(mainDispatcher) { + state.value = JsonTreeDifferState.Loading + } + } + val originalJsonTreeListDeferred = async { getJsonTreeList(original) } val revisedJsonTreeListDeferred = async { getJsonTreeList(revised) } @@ -50,16 +57,17 @@ internal class JsonTreeDiffer( } } - val diffRowGenerator = object : DiffTagGenerator { + val diffTagGenerator = object : DiffTagGenerator { override fun generateClose(tag: DiffRow.Tag): String = inlineDiffTagClosed override fun generateOpen(tag: DiffRow.Tag): String = inlineDiffTagOpen } val diffRows = DiffRowGenerator( columnWidth = 0, // Needs to be 0, otherwise HTML linebreaks will be inserted in diff lines. - showInlineDiffs = true, - newTag = diffRowGenerator, - oldTag = diffRowGenerator, + showInlineDiffs = showInlineDiffs, + lineNormalizer = DiffLineNormalizer { line -> line }, + newTag = diffTagGenerator, + oldTag = diffTagGenerator, ).generateDiffRows( originalJsonTreeList.map { it.toRenderString() }, revisedJsonTreeList.map { it.toRenderString() } @@ -69,9 +77,7 @@ internal class JsonTreeDiffer( val usedRevisedIds = mutableListOf() val diffElements = diffRows.map { diffRow -> - println("DiffRow: ${diffRow.tag}: ${diffRow.oldLine} -> ${diffRow.newLine}") - - val inlineDiffsIndices = if(diffRow.tag == DiffRow.Tag.CHANGE) { + val (oldLineDiffIndices, newLineDiffIndices) = if(diffRow.tag == DiffRow.Tag.CHANGE) { val oldLineIndices = diffRow.oldLine.findInlineDiffTagIndices() val newLineIndices = diffRow.newLine.findInlineDiffTagIndices() Pair(oldLineIndices, newLineIndices) @@ -83,20 +89,19 @@ internal class JsonTreeDiffer( oldLine = diffRow.oldLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, ""), newLine = diffRow.newLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, "") ) - println("Stripped: ${strippedTagDiffRow.tag}: ${strippedTagDiffRow.oldLine} -> ${strippedTagDiffRow.newLine}") val originalDiffElement = getOriginalDiffElement( diffRow = strippedTagDiffRow, usedIds = usedOriginalIds, originalJsonTreeList = originalJsonTreeList, - inlineDiffsIndices = inlineDiffsIndices.first + inlineDiffsIndices = oldLineDiffIndices ) val revisedDiffElement = getRevisedDiffElement( diffRow = strippedTagDiffRow, usedIds = usedRevisedIds, revisedJsonTreeList = revisedJsonTreeList, - inlineDiffsIndices = inlineDiffsIndices.second + inlineDiffsIndices = newLineDiffIndices ) Pair(originalDiffElement, revisedDiffElement) @@ -184,8 +189,8 @@ internal class JsonTreeDiffer( private suspend fun getJsonTreeList(json: String): JsonTreeParserState { val originalParser = JsonTreeParser( json = json, - defaultDispatcher = Dispatchers.Default, - mainDispatcher = Dispatchers.Main + defaultDispatcher = defaultDispatcher, + mainDispatcher = mainDispatcher ).also { it.init(TreeState.EXPANDED) } return when(val state = originalParser.state.value) { diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/Extensions.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/JsonTreeElementExtensions.kt similarity index 100% rename from jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/Extensions.kt rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/JsonTreeElementExtensions.kt diff --git a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ExtensionsTest.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeElementExtensionsTest.kt similarity index 99% rename from jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ExtensionsTest.kt rename to jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeElementExtensionsTest.kt index c8ea1a7..4bc34d5 100644 --- a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ExtensionsTest.kt +++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeElementExtensionsTest.kt @@ -10,7 +10,7 @@ import kotlinx.serialization.json.JsonPrimitive import kotlin.test.Test import kotlin.test.assertEquals -public class ExtensionsTest { +public class JsonTreeElementExtensionsTest { @Test public fun expand_object_with_expansion_none_should_not_expand_children() { diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt index db3d82a..31a5a05 100644 --- a/sample/src/commonMain/kotlin/App.kt +++ b/sample/src/commonMain/kotlin/App.kt @@ -104,6 +104,7 @@ private fun MainScreen() { var showItemCount: Boolean by remember { mutableStateOf(true) } var expandSingleChildren: Boolean by remember { mutableStateOf(true) } var showDiff: Boolean by remember { mutableStateOf(false) } + var showInlineDiffs: Boolean by remember { mutableStateOf(true) } val searchState = rememberSearchState() val searchQuery by remember(searchState.query) { mutableStateOf(searchState.query.orEmpty()) } val coroutineScope = rememberCoroutineScope() @@ -187,6 +188,13 @@ private fun MainScreen() { ) { Text(text = if (showDiff) "Hide diff" else "Show diff") } + + Button( + modifier = Modifier.padding(horizontal = 8.dp), + onClick = { showInlineDiffs = !showInlineDiffs } + ) { + Text(text = if (showInlineDiffs) "Hide inline diffs" else "Show inline diffs") + } } Spacer(Modifier.height(8.dp)) @@ -221,6 +229,7 @@ private fun MainScreen() { if(showDiff) { JsonDiff( originalJson = json, + showInlineDiffs = showInlineDiffs, colors = colors, ) } @@ -355,6 +364,7 @@ private fun MainScreen() { @Composable private fun JsonDiff( originalJson: String, + showInlineDiffs: Boolean, colors: TreeColors ) { var error by remember(originalJson) { mutableStateOf(null) } @@ -380,6 +390,7 @@ private fun JsonDiff( ), originalJson = originalJson, revisedJson = complexJsonRevised, + showInlineDiffs = showInlineDiffs, onLoading = { Box( modifier = Modifier From 8cc8ccbd9aa6242bfb607ae4f105f7f62d88fe4e Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sun, 15 Mar 2026 14:38:56 +0000 Subject: [PATCH 12/32] Add tests for toRenderString --- .../jsontree/JsonTreeElementExtensionsTest.kt | 303 ++++++++++++++++++ 1 file changed, 303 insertions(+) diff --git a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeElementExtensionsTest.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeElementExtensionsTest.kt index 4bc34d5..ddd4a53 100644 --- a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeElementExtensionsTest.kt +++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeElementExtensionsTest.kt @@ -6,6 +6,7 @@ import com.sebastianneubauer.jsontree.util.Expansion import com.sebastianneubauer.jsontree.util.collapse import com.sebastianneubauer.jsontree.util.expand import com.sebastianneubauer.jsontree.util.toList +import com.sebastianneubauer.jsontree.util.toRenderString import kotlinx.serialization.json.JsonPrimitive import kotlin.test.Test import kotlin.test.assertEquals @@ -124,6 +125,308 @@ public class JsonTreeElementExtensionsTest { ) } + @Test + public fun toRenderString_object_with_key_and_object_parent_should_render_with_key() { + val element = Object( + id = "", + level = 0, + state = TreeState.COLLAPSED, + children = emptyMap(), + isLastItem = true, + key = "myKey", + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "\"myKey\": {" + ) + } + + @Test + public fun toRenderString_object_with_key_and_array_parent_should_render_without_key() { + val element = Object( + id = "", + level = 0, + state = TreeState.COLLAPSED, + children = emptyMap(), + isLastItem = true, + key = "0", + parentType = JsonTreeElement.ParentType.ARRAY + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "{" + ) + } + + @Test + public fun toRenderString_object_without_key_should_render_without_key() { + val element = Object( + id = "", + level = 0, + state = TreeState.COLLAPSED, + children = emptyMap(), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "{" + ) + } + + @Test + public fun toRenderString_array_with_key_and_object_parent_should_render_with_key() { + val element = Array( + id = "", + level = 0, + state = TreeState.COLLAPSED, + children = emptyMap(), + isLastItem = true, + key = "myArray", + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "\"myArray\": [" + ) + } + + @Test + public fun toRenderString_array_with_key_and_array_parent_should_render_without_key() { + val element = Array( + id = "", + level = 0, + state = TreeState.COLLAPSED, + children = emptyMap(), + isLastItem = true, + key = "0", + parentType = JsonTreeElement.ParentType.ARRAY + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "[" + ) + } + + @Test + public fun toRenderString_array_without_key_should_render_without_key() { + val element = Array( + id = "", + level = 0, + state = TreeState.COLLAPSED, + children = emptyMap(), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "[" + ) + } + + @Test + public fun toRenderString_primitive_with_key_and_object_parent_not_last_should_render_with_key_and_comma() { + val element = JsonTreeElement.Primitive( + id = "", + level = 0, + isLastItem = false, + key = "myProp", + value = JsonPrimitive("test"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "\"myProp\": \"test\"," + ) + } + + @Test + public fun toRenderString_primitive_with_key_and_object_parent_last_should_render_with_key_and_without_comma() { + val element = JsonTreeElement.Primitive( + id = "", + level = 0, + isLastItem = true, + key = "myProp", + value = JsonPrimitive("test"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "\"myProp\": \"test\"" + ) + } + + @Test + public fun toRenderString_primitive_with_key_and_array_parent_not_last_should_render_without_key_and_with_comma() { + val element = JsonTreeElement.Primitive( + id = "", + level = 0, + isLastItem = false, + key = "0", + value = JsonPrimitive(42), + parentType = JsonTreeElement.ParentType.ARRAY + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "42," + ) + } + + @Test + public fun toRenderString_primitive_with_key_and_array_parent_last_should_render_without_key_and_without_comma() { + val element = JsonTreeElement.Primitive( + id = "", + level = 0, + isLastItem = true, + key = "0", + value = JsonPrimitive(42), + parentType = JsonTreeElement.ParentType.ARRAY + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "42" + ) + } + + @Test + public fun toRenderString_primitive_without_key_not_last_should_render_without_key_and_with_comma() { + val element = JsonTreeElement.Primitive( + id = "", + level = 0, + isLastItem = false, + key = null, + value = JsonPrimitive(true), + parentType = JsonTreeElement.ParentType.NONE + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "true," + ) + } + + @Test + public fun toRenderString_primitive_without_key_last_should_render_without_key_and_without_comma() { + val element = JsonTreeElement.Primitive( + id = "", + level = 0, + isLastItem = true, + key = null, + value = JsonPrimitive(false), + parentType = JsonTreeElement.ParentType.NONE + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "false" + ) + } + + @Test + public fun toRenderString_end_bracket_array_not_last_should_render_with_comma() { + val element = JsonTreeElement.EndBracket( + id = "", + level = 0, + isLastItem = false, + type = JsonTreeElement.EndBracket.Type.ARRAY + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "]," + ) + } + + @Test + public fun toRenderString_end_bracket_array_last_should_render_without_comma() { + val element = JsonTreeElement.EndBracket( + id = "", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.ARRAY + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "]" + ) + } + + @Test + public fun toRenderString_end_bracket_object_not_last_should_render_with_comma() { + val element = JsonTreeElement.EndBracket( + id = "", + level = 0, + isLastItem = false, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "}," + ) + } + + @Test + public fun toRenderString_end_bracket_object_last_should_render_without_comma() { + val element = JsonTreeElement.EndBracket( + id = "", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + + val result = element.toRenderString() + + assertEquals( + actual = result, + expected = "}" + ) + } + private object ExpandTestData { val primitive1 = JsonTreeElement.Primitive( id = "primitive1", From f5d84e1c50a7bffd9ed35e71966f7134a3936fb5 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Mon, 16 Mar 2026 09:21:48 +0000 Subject: [PATCH 13/32] Add tests for JsonTreeDifferTest --- .../jsontree/diff/JsonTreeDifferTest.kt | 1162 +++++++++++++++++ 1 file changed, 1162 insertions(+) create mode 100644 jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt diff --git a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt new file mode 100644 index 0000000..8f7a334 --- /dev/null +++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt @@ -0,0 +1,1162 @@ +package com.sebastianneubauer.jsontree.diff + +import com.sebastianneubauer.jsontree.JsonTreeElement +import com.sebastianneubauer.jsontree.TreeState +import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +public class JsonTreeDifferTest { + + @OptIn(ExperimentalCoroutinesApi::class) + private fun underTest() = JsonTreeDiffer( + defaultDispatcher = UnconfinedTestDispatcher(), + mainDispatcher = UnconfinedTestDispatcher() + ) + + @Test + public fun initial_state_is_loading(): TestResult = runTest { + assertEquals( + expected = JsonTreeDifferState.Loading, + actual = underTest().state.value + ) + } + + @Test + public fun diff_with_identical_json_shows_all_equal_elements(): TestResult = runTest { + val json = """{"name": "value"}""" + val differ = underTest() + + differ.diff( + original = json, + revised = json, + showInlineDiffs = false + ) + + val primitive = JsonTreeElement.Primitive( + id = "1", + level = 1, + isLastItem = true, + key = "name", + value = JsonPrimitive("value"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + assertEquals( + expected = JsonTreeDifferState.Ready( + diffElements = listOf( + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.Collapsable.Object( + id = "2", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("name" to primitive), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.Collapsable.Object( + id = "2", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("name" to primitive), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + ) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = primitive), + JsonDiffElement.Equal(jsonTreeElement = primitive) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ) + ) + ), + actual = differ.state.value + ) + } + + @Test + public fun diff_with_changed_value_shows_change_elements(): TestResult = runTest { + val original = """{"name": "oldValue"}""" + val revised = """{"name": "newValue"}""" + val differ = underTest() + + differ.diff( + original = original, + revised = revised, + showInlineDiffs = false + ) + + val originalPrimitive = JsonTreeElement.Primitive( + id = "1", + level = 1, + isLastItem = true, + key = "name", + value = JsonPrimitive("oldValue"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedPrimitive = JsonTreeElement.Primitive( + id = "1", + level = 1, + isLastItem = true, + key = "name", + value = JsonPrimitive("newValue"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + assertEquals( + expected = JsonTreeDifferState.Ready( + diffElements = listOf( + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.Collapsable.Object( + id = "2", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("name" to originalPrimitive), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.Collapsable.Object( + id = "2", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("name" to revisedPrimitive), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + ) + ), + Pair( + JsonDiffElement.Change( + jsonTreeElement = originalPrimitive, + inlineDiffIndices = emptyList() + ), + JsonDiffElement.Change( + jsonTreeElement = revisedPrimitive, + inlineDiffIndices = emptyList() + ) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ) + ) + ), + actual = differ.state.value + ) + } + + @Test + public fun diff_with_added_field_shows_insertion(): TestResult = runTest { + val original = """{"name": "value"}""" + val revised = """{"age": 42, "name": "value"}""" + val differ = underTest() + + differ.diff( + original = original, + revised = revised, + showInlineDiffs = false + ) + + val originalPrimitive = JsonTreeElement.Primitive( + id = "1", + level = 1, + isLastItem = true, + key = "name", + value = JsonPrimitive("value"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedAgePrimitive = JsonTreeElement.Primitive( + id = "1", + level = 1, + isLastItem = false, + key = "age", + value = JsonPrimitive(42), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedNamePrimitive = JsonTreeElement.Primitive( + id = "2", + level = 1, + isLastItem = true, + key = "name", + value = JsonPrimitive("value"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + assertEquals( + expected = JsonTreeDifferState.Ready( + diffElements = listOf( + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.Collapsable.Object( + id = "2", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("name" to originalPrimitive), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.Collapsable.Object( + id = "3", + level = 0, + state = TreeState.EXPANDED, + children = mapOf( + "age" to revisedAgePrimitive, + "name" to revisedNamePrimitive + ), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + ) + ), + Pair( + JsonDiffElement.Insertion(jsonTreeElement = null), + JsonDiffElement.Insertion(jsonTreeElement = revisedAgePrimitive) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = originalPrimitive), + JsonDiffElement.Equal(jsonTreeElement = revisedNamePrimitive) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ) + ) + ), + actual = differ.state.value + ) + } + + @Test + public fun diff_with_removed_field_shows_deletion(): TestResult = runTest { + val original = """{"age": 42, "name": "value"}""" + val revised = """{"name": "value"}""" + val differ = underTest() + + differ.diff( + original = original, + revised = revised, + showInlineDiffs = false + ) + + val originalAgePrimitive = JsonTreeElement.Primitive( + id = "1", + level = 1, + isLastItem = false, + key = "age", + value = JsonPrimitive(42), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val originalNamePrimitive = JsonTreeElement.Primitive( + id = "2", + level = 1, + isLastItem = true, + key = "name", + value = JsonPrimitive("value"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedPrimitive = JsonTreeElement.Primitive( + id = "1", + level = 1, + isLastItem = true, + key = "name", + value = JsonPrimitive("value"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + assertEquals( + expected = JsonTreeDifferState.Ready( + diffElements = listOf( + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.Collapsable.Object( + id = "3", + level = 0, + state = TreeState.EXPANDED, + children = mapOf( + "age" to originalAgePrimitive, + "name" to originalNamePrimitive + ), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.Collapsable.Object( + id = "2", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("name" to revisedPrimitive), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + ) + ), + Pair( + JsonDiffElement.Deletion(jsonTreeElement = originalAgePrimitive), + JsonDiffElement.Deletion(jsonTreeElement = null) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = originalNamePrimitive), + JsonDiffElement.Equal(jsonTreeElement = revisedPrimitive) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ) + ) + ), + actual = differ.state.value + ) + } + + @Test + public fun diff_with_inline_diffs_enabled_shows_inline_diff_indices(): TestResult = runTest { + val original = """{"name": "oldValue"}""" + val revised = """{"name": "newValue"}""" + val differ = underTest() + + differ.diff( + original = original, + revised = revised, + showInlineDiffs = true + ) + + val originalPrimitive = JsonTreeElement.Primitive( + id = "1", + level = 1, + isLastItem = true, + key = "name", + value = JsonPrimitive("oldValue"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedPrimitive = JsonTreeElement.Primitive( + id = "1", + level = 1, + isLastItem = true, + key = "name", + value = JsonPrimitive("newValue"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + assertEquals( + expected = JsonTreeDifferState.Ready( + diffElements = listOf( + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.Collapsable.Object( + id = "2", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("name" to originalPrimitive), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.Collapsable.Object( + id = "2", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("name" to revisedPrimitive), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + ) + ), + Pair( + JsonDiffElement.Change( + jsonTreeElement = originalPrimitive, + inlineDiffIndices = listOf(Pair(9, 12)) + ), + JsonDiffElement.Change( + jsonTreeElement = revisedPrimitive, + inlineDiffIndices = listOf(Pair(9, 12)) + ) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ) + ) + ), + actual = differ.state.value + ) + } + + @Test + public fun diff_with_invalid_original_json_shows_original_error(): TestResult = runTest { + val differ = underTest() + + differ.diff( + original = "", + revised = """{"name": "value"}""", + showInlineDiffs = false + ) + + val state = differ.state.value + assertTrue(state is JsonTreeDifferState.Error.OriginalJsonError) + } + + @Test + public fun diff_with_invalid_revised_json_shows_revised_error(): TestResult = runTest { + val differ = underTest() + + differ.diff( + original = """{"name": "value"}""", + revised = "", + showInlineDiffs = false + ) + + val state = differ.state.value + assertTrue(state is JsonTreeDifferState.Error.RevisedJsonError) + } + + @Test + public fun diff_with_nested_object_identical_shows_all_elements(): TestResult = runTest { + val json = """{"user": {"name": "John"}}""" + val differ = underTest() + + differ.diff( + original = json, + revised = json, + showInlineDiffs = false + ) + + val userObjectPrimitive = JsonTreeElement.Primitive( + id = "1", + level = 2, + isLastItem = true, + key = "name", + value = JsonPrimitive("John"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val userObject = JsonTreeElement.Collapsable.Object( + id = "2", + level = 1, + state = TreeState.EXPANDED, + children = mapOf("name" to userObjectPrimitive), + isLastItem = true, + key = "user", + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val rootObject = JsonTreeElement.Collapsable.Object( + id = "3", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("user" to userObject), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + + assertEquals( + expected = JsonTreeDifferState.Ready( + diffElements = listOf( + Pair( + JsonDiffElement.Equal(jsonTreeElement = rootObject), + JsonDiffElement.Equal(jsonTreeElement = rootObject) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = userObject), + JsonDiffElement.Equal(jsonTreeElement = userObject) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = userObjectPrimitive), + JsonDiffElement.Equal(jsonTreeElement = userObjectPrimitive) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ) + ) + ), + actual = differ.state.value + ) + } + + @Test + public fun diff_with_nested_object_value_change_shows_change(): TestResult = runTest { + val original = """{"user": {"name": "John"}}""" + val revised = """{"user": {"name": "Jane"}}""" + val differ = underTest() + + differ.diff( + original = original, + revised = revised, + showInlineDiffs = false + ) + + val originalUserPrimitive = JsonTreeElement.Primitive( + id = "1", + level = 2, + isLastItem = true, + key = "name", + value = JsonPrimitive("John"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedUserPrimitive = JsonTreeElement.Primitive( + id = "1", + level = 2, + isLastItem = true, + key = "name", + value = JsonPrimitive("Jane"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val originalUserObject = JsonTreeElement.Collapsable.Object( + id = "2", + level = 1, + state = TreeState.EXPANDED, + children = mapOf("name" to originalUserPrimitive), + isLastItem = true, + key = "user", + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedUserObject = JsonTreeElement.Collapsable.Object( + id = "2", + level = 1, + state = TreeState.EXPANDED, + children = mapOf("name" to revisedUserPrimitive), + isLastItem = true, + key = "user", + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val originalRootObject = JsonTreeElement.Collapsable.Object( + id = "3", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("user" to originalUserObject), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + + val revisedRootObject = JsonTreeElement.Collapsable.Object( + id = "3", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("user" to revisedUserObject), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + + assertEquals( + expected = JsonTreeDifferState.Ready( + diffElements = listOf( + Pair( + JsonDiffElement.Equal(jsonTreeElement = originalRootObject), + JsonDiffElement.Equal(jsonTreeElement = revisedRootObject) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = originalUserObject), + JsonDiffElement.Equal(jsonTreeElement = revisedUserObject) + ), + Pair( + JsonDiffElement.Change( + jsonTreeElement = originalUserPrimitive, + inlineDiffIndices = emptyList() + ), + JsonDiffElement.Change( + jsonTreeElement = revisedUserPrimitive, + inlineDiffIndices = emptyList() + ) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ) + ) + ), + actual = differ.state.value + ) + } + + @Test + public fun diff_with_deeply_nested_objects_preserves_structure(): TestResult = runTest { + val json = """{"a": {"b": {"c": "value"}}}""" + val differ = underTest() + + differ.diff( + original = json, + revised = json, + showInlineDiffs = false + ) + + val cPrimitive = JsonTreeElement.Primitive( + id = "1", + level = 3, + isLastItem = true, + key = "c", + value = JsonPrimitive("value"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val bObject = JsonTreeElement.Collapsable.Object( + id = "2", + level = 2, + state = TreeState.EXPANDED, + children = mapOf("c" to cPrimitive), + isLastItem = true, + key = "b", + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val aObject = JsonTreeElement.Collapsable.Object( + id = "3", + level = 1, + state = TreeState.EXPANDED, + children = mapOf("b" to bObject), + isLastItem = true, + key = "a", + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val rootObject = JsonTreeElement.Collapsable.Object( + id = "4", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("a" to aObject), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + + assertEquals( + expected = JsonTreeDifferState.Ready( + diffElements = listOf( + Pair( + JsonDiffElement.Equal(jsonTreeElement = rootObject), + JsonDiffElement.Equal(jsonTreeElement = rootObject) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = aObject), + JsonDiffElement.Equal(jsonTreeElement = aObject) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = bObject), + JsonDiffElement.Equal(jsonTreeElement = bObject) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = cPrimitive), + JsonDiffElement.Equal(jsonTreeElement = cPrimitive) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 2, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 2, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "4-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "4-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ) + ) + ), + actual = differ.state.value + ) + } + + @Test + public fun diff_with_nested_object_insertion_shows_insertion(): TestResult = runTest { + val original = """{"user": {"name": "John"}}""" + val revised = """{"user": {"age": 30, "name": "John"}}""" + val differ = underTest() + + differ.diff( + original = original, + revised = revised, + showInlineDiffs = false + ) + + val originalNamePrimitive = JsonTreeElement.Primitive( + id = "1", + level = 2, + isLastItem = true, + key = "name", + value = JsonPrimitive("John"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedAgePrimitive = JsonTreeElement.Primitive( + id = "1", + level = 2, + isLastItem = false, + key = "age", + value = JsonPrimitive(30), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedNamePrimitive = JsonTreeElement.Primitive( + id = "2", + level = 2, + isLastItem = true, + key = "name", + value = JsonPrimitive("John"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val originalUserObject = JsonTreeElement.Collapsable.Object( + id = "2", + level = 1, + state = TreeState.EXPANDED, + children = mapOf("name" to originalNamePrimitive), + isLastItem = true, + key = "user", + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedUserObject = JsonTreeElement.Collapsable.Object( + id = "3", + level = 1, + state = TreeState.EXPANDED, + children = mapOf( + "age" to revisedAgePrimitive, + "name" to revisedNamePrimitive + ), + isLastItem = true, + key = "user", + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val originalRootObject = JsonTreeElement.Collapsable.Object( + id = "3", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("user" to originalUserObject), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + + val revisedRootObject = JsonTreeElement.Collapsable.Object( + id = "4", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("user" to revisedUserObject), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + + assertEquals( + expected = JsonTreeDifferState.Ready( + diffElements = listOf( + Pair( + JsonDiffElement.Equal(jsonTreeElement = originalRootObject), + JsonDiffElement.Equal(jsonTreeElement = revisedRootObject) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = originalUserObject), + JsonDiffElement.Equal(jsonTreeElement = revisedUserObject) + ), + Pair( + JsonDiffElement.Insertion(jsonTreeElement = null), + JsonDiffElement.Insertion(jsonTreeElement = revisedAgePrimitive) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = originalNamePrimitive), + JsonDiffElement.Equal(jsonTreeElement = revisedNamePrimitive) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "4-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ) + ) + ), + actual = differ.state.value + ) + } + + @Test + public fun diff_with_nested_object_deletion_shows_deletion(): TestResult = runTest { + val original = """{"user": {"age": 30, "name": "John"}}""" + val revised = """{"user": {"name": "John"}}""" + val differ = underTest() + + differ.diff( + original = original, + revised = revised, + showInlineDiffs = false + ) + + val originalAgePrimitive = JsonTreeElement.Primitive( + id = "1", + level = 2, + isLastItem = false, + key = "age", + value = JsonPrimitive(30), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val originalNamePrimitive = JsonTreeElement.Primitive( + id = "2", + level = 2, + isLastItem = true, + key = "name", + value = JsonPrimitive("John"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedNamePrimitive = JsonTreeElement.Primitive( + id = "1", + level = 2, + isLastItem = true, + key = "name", + value = JsonPrimitive("John"), + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val originalUserObject = JsonTreeElement.Collapsable.Object( + id = "3", + level = 1, + state = TreeState.EXPANDED, + children = mapOf( + "age" to originalAgePrimitive, + "name" to originalNamePrimitive + ), + isLastItem = true, + key = "user", + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val revisedUserObject = JsonTreeElement.Collapsable.Object( + id = "2", + level = 1, + state = TreeState.EXPANDED, + children = mapOf("name" to revisedNamePrimitive), + isLastItem = true, + key = "user", + parentType = JsonTreeElement.ParentType.OBJECT + ) + + val originalRootObject = JsonTreeElement.Collapsable.Object( + id = "4", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("user" to originalUserObject), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + + val revisedRootObject = JsonTreeElement.Collapsable.Object( + id = "3", + level = 0, + state = TreeState.EXPANDED, + children = mapOf("user" to revisedUserObject), + isLastItem = true, + key = null, + parentType = JsonTreeElement.ParentType.NONE + ) + + assertEquals( + expected = JsonTreeDifferState.Ready( + diffElements = listOf( + Pair( + JsonDiffElement.Equal(jsonTreeElement = originalRootObject), + JsonDiffElement.Equal(jsonTreeElement = revisedRootObject) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = originalUserObject), + JsonDiffElement.Equal(jsonTreeElement = revisedUserObject) + ), + Pair( + JsonDiffElement.Deletion(jsonTreeElement = originalAgePrimitive), + JsonDiffElement.Deletion(jsonTreeElement = null) + ), + Pair( + JsonDiffElement.Equal(jsonTreeElement = originalNamePrimitive), + JsonDiffElement.Equal(jsonTreeElement = revisedNamePrimitive) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "2-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ), + Pair( + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "4-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ), + JsonDiffElement.Equal( + jsonTreeElement = JsonTreeElement.EndBracket( + id = "3-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ) + ) + ) + ) + ), + actual = differ.state.value + ) + } +} \ No newline at end of file From e76ec57cea4d7006878ef6d29febbc48759353aa Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Mon, 16 Mar 2026 17:06:52 +0000 Subject: [PATCH 14/32] Optimize by using maps --- .../jsontree/diff/JsonTreeDiffer.kt | 98 +++++++++++++------ 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index 50351cd..d433f1c 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -11,10 +11,15 @@ import io.github.petertrr.diffutils.text.DiffRow import io.github.petertrr.diffutils.text.DiffRowGenerator import io.github.petertrr.diffutils.text.DiffTagGenerator import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.NonCancellable.start import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import kotlin.coroutines.resume +import kotlin.time.Clock internal class JsonTreeDiffer( val defaultDispatcher: CoroutineDispatcher, @@ -57,6 +62,9 @@ internal class JsonTreeDiffer( } } + val originalJsonTreeMapDeferred = getJsonTreeMapDeferred(originalJsonTreeList) + val revisedJsonTreeMapDeferred = getJsonTreeMapDeferred(revisedJsonTreeList) + val diffTagGenerator = object : DiffTagGenerator { override fun generateClose(tag: DiffRow.Tag): String = inlineDiffTagClosed override fun generateOpen(tag: DiffRow.Tag): String = inlineDiffTagOpen @@ -73,8 +81,11 @@ internal class JsonTreeDiffer( revisedJsonTreeList.map { it.toRenderString() } ) - val usedOriginalIds = mutableListOf() - val usedRevisedIds = mutableListOf() + val originalJsonTreeMap = originalJsonTreeMapDeferred.await() + val revisedJsonTreeMap = revisedJsonTreeMapDeferred.await() + + val usedOriginalIds = mutableMapOf>() + val usedRevisedIds = mutableMapOf>() val diffElements = diffRows.map { diffRow -> val (oldLineDiffIndices, newLineDiffIndices) = if(diffRow.tag == DiffRow.Tag.CHANGE) { @@ -90,21 +101,31 @@ internal class JsonTreeDiffer( newLine = diffRow.newLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, "") ) - val originalDiffElement = getOriginalDiffElement( - diffRow = strippedTagDiffRow, - usedIds = usedOriginalIds, - originalJsonTreeList = originalJsonTreeList, - inlineDiffsIndices = oldLineDiffIndices - ) + val originalDiffElementDeferred = async { + suspendCancellableCoroutine { continuation -> + val result = getOriginalDiffElement( + diffRow = strippedTagDiffRow, + usedIds = usedOriginalIds, + originalJsonTreeMap = originalJsonTreeMap, + inlineDiffsIndices = oldLineDiffIndices + ) + continuation.resume(result) + } + } - val revisedDiffElement = getRevisedDiffElement( - diffRow = strippedTagDiffRow, - usedIds = usedRevisedIds, - revisedJsonTreeList = revisedJsonTreeList, - inlineDiffsIndices = newLineDiffIndices - ) + val revisedDiffElementDeferred = async { + suspendCancellableCoroutine { continuation -> + val result = getRevisedDiffElement( + diffRow = strippedTagDiffRow, + usedIds = usedRevisedIds, + revisedJsonTreeMap = revisedJsonTreeMap, + inlineDiffsIndices = newLineDiffIndices + ) + continuation.resume(result) + } + } - Pair(originalDiffElement, revisedDiffElement) + Pair(originalDiffElementDeferred.await(), revisedDiffElementDeferred.await()) } withContext(mainDispatcher) { @@ -114,27 +135,37 @@ internal class JsonTreeDiffer( } } + private fun CoroutineScope.getJsonTreeMapDeferred( + list: List + ): Deferred>> = async { + suspendCancellableCoroutine { continuation -> + val result = list.groupBy { it.toRenderString() } + continuation.resume(result) + } + } + private fun getOriginalDiffElement( diffRow: DiffRow, - usedIds: MutableList, - originalJsonTreeList: List, + usedIds: MutableMap>, + originalJsonTreeMap: Map>, inlineDiffsIndices: List> ): JsonDiffElement { fun findJsonTreeElement(diffLine: String): JsonTreeElement { - return originalJsonTreeList.first { jsonTreeElement -> - diffLine == jsonTreeElement.toRenderString() && jsonTreeElement.id !in usedIds - } + val jsonTreeElements = originalJsonTreeMap.getValue(diffLine) + return jsonTreeElements.first { it.id !in usedIds[diffLine].orEmpty() } } return when(diffRow.tag) { DiffRow.Tag.EQUAL -> { val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) - usedIds.add(jsonTreeElement.id) + val currentIds = usedIds[diffRow.oldLine] ?: mutableListOf() + usedIds[diffRow.oldLine] = currentIds.apply { add(jsonTreeElement.id) } JsonDiffElement.Equal(jsonTreeElement) } DiffRow.Tag.CHANGE -> { val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) - usedIds.add(jsonTreeElement.id) + val currentIds = usedIds[diffRow.oldLine] ?: mutableListOf() + usedIds[diffRow.oldLine] = currentIds.apply { add(jsonTreeElement.id) } JsonDiffElement.Change( jsonTreeElement = jsonTreeElement, inlineDiffIndices = inlineDiffsIndices @@ -145,7 +176,8 @@ internal class JsonTreeDiffer( } DiffRow.Tag.DELETE -> { val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) - usedIds.add(jsonTreeElement.id) + val currentIds = usedIds[diffRow.oldLine] ?: mutableListOf() + usedIds[diffRow.oldLine] = currentIds.apply { add(jsonTreeElement.id) } JsonDiffElement.Deletion(jsonTreeElement) } } @@ -153,25 +185,26 @@ internal class JsonTreeDiffer( private fun getRevisedDiffElement( diffRow: DiffRow, - usedIds: MutableList, - revisedJsonTreeList: List, + usedIds: MutableMap>, + revisedJsonTreeMap: Map>, inlineDiffsIndices: List> ): JsonDiffElement { fun findJsonTreeElement(diffLine: String): JsonTreeElement { - return revisedJsonTreeList.first { jsonTreeElement -> - diffLine == jsonTreeElement.toRenderString() && jsonTreeElement.id !in usedIds - } + val jsonTreeElements = revisedJsonTreeMap.getValue(diffLine) + return jsonTreeElements.first { it.id !in usedIds[diffLine].orEmpty() } } return when(diffRow.tag) { DiffRow.Tag.EQUAL -> { val jsonTreeElement = findJsonTreeElement(diffRow.newLine) - usedIds.add(jsonTreeElement.id) + val currentIds = usedIds[diffRow.newLine] ?: mutableListOf() + usedIds[diffRow.newLine] = currentIds.apply { add(jsonTreeElement.id) } JsonDiffElement.Equal(jsonTreeElement) } DiffRow.Tag.CHANGE -> { val jsonTreeElement = findJsonTreeElement(diffRow.newLine) - usedIds.add(jsonTreeElement.id) + val currentIds = usedIds[diffRow.newLine] ?: mutableListOf() + usedIds[diffRow.newLine] = currentIds.apply { add(jsonTreeElement.id) } JsonDiffElement.Change( jsonTreeElement = jsonTreeElement, inlineDiffIndices = inlineDiffsIndices @@ -179,7 +212,8 @@ internal class JsonTreeDiffer( } DiffRow.Tag.INSERT -> { val jsonTreeElement = findJsonTreeElement(diffRow.newLine) - usedIds.add(jsonTreeElement.id) + val currentIds = usedIds[diffRow.newLine] ?: mutableListOf() + usedIds[diffRow.newLine] = currentIds.apply { add(jsonTreeElement.id) } JsonDiffElement.Insertion(jsonTreeElement) } DiffRow.Tag.DELETE -> JsonDiffElement.Deletion(null) From f1f142a7be19dd5636a1f7c84631328bd8ec6cf5 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Mon, 16 Mar 2026 17:34:04 +0000 Subject: [PATCH 15/32] Clean up code --- .../jsontree/diff/JsonTreeDiffer.kt | 109 +++++++++--------- 1 file changed, 52 insertions(+), 57 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index d433f1c..b2e1855 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -13,13 +13,12 @@ import io.github.petertrr.diffutils.text.DiffTagGenerator import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred -import kotlinx.coroutines.NonCancellable.start import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import kotlin.collections.orEmpty import kotlin.coroutines.resume -import kotlin.time.Clock internal class JsonTreeDiffer( val defaultDispatcher: CoroutineDispatcher, @@ -62,8 +61,8 @@ internal class JsonTreeDiffer( } } - val originalJsonTreeMapDeferred = getJsonTreeMapDeferred(originalJsonTreeList) - val revisedJsonTreeMapDeferred = getJsonTreeMapDeferred(revisedJsonTreeList) + val originalJsonTreeMapDeferred = runAsync { originalJsonTreeList.groupBy { it.toRenderString() } } + val revisedJsonTreeMapDeferred = runAsync { revisedJsonTreeList.groupBy { it.toRenderString() } } val diffTagGenerator = object : DiffTagGenerator { override fun generateClose(tag: DiffRow.Tag): String = inlineDiffTagClosed @@ -101,28 +100,22 @@ internal class JsonTreeDiffer( newLine = diffRow.newLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, "") ) - val originalDiffElementDeferred = async { - suspendCancellableCoroutine { continuation -> - val result = getOriginalDiffElement( - diffRow = strippedTagDiffRow, - usedIds = usedOriginalIds, - originalJsonTreeMap = originalJsonTreeMap, - inlineDiffsIndices = oldLineDiffIndices - ) - continuation.resume(result) - } + val originalDiffElementDeferred = runAsync { + getOriginalDiffElement( + diffRow = strippedTagDiffRow, + usedIds = usedOriginalIds, + originalJsonTreeMap = originalJsonTreeMap, + inlineDiffsIndices = oldLineDiffIndices + ) } - val revisedDiffElementDeferred = async { - suspendCancellableCoroutine { continuation -> - val result = getRevisedDiffElement( - diffRow = strippedTagDiffRow, - usedIds = usedRevisedIds, - revisedJsonTreeMap = revisedJsonTreeMap, - inlineDiffsIndices = newLineDiffIndices - ) - continuation.resume(result) - } + val revisedDiffElementDeferred = runAsync { + getRevisedDiffElement( + diffRow = strippedTagDiffRow, + usedIds = usedRevisedIds, + revisedJsonTreeMap = revisedJsonTreeMap, + inlineDiffsIndices = newLineDiffIndices + ) } Pair(originalDiffElementDeferred.await(), revisedDiffElementDeferred.await()) @@ -135,37 +128,31 @@ internal class JsonTreeDiffer( } } - private fun CoroutineScope.getJsonTreeMapDeferred( - list: List - ): Deferred>> = async { - suspendCancellableCoroutine { continuation -> - val result = list.groupBy { it.toRenderString() } - continuation.resume(result) - } - } - private fun getOriginalDiffElement( diffRow: DiffRow, usedIds: MutableMap>, originalJsonTreeMap: Map>, inlineDiffsIndices: List> ): JsonDiffElement { - fun findJsonTreeElement(diffLine: String): JsonTreeElement { - val jsonTreeElements = originalJsonTreeMap.getValue(diffLine) - return jsonTreeElements.first { it.id !in usedIds[diffLine].orEmpty() } + fun findJsonTreeElement(): JsonTreeElement { + val jsonTreeElements = originalJsonTreeMap.getValue(diffRow.oldLine) + return jsonTreeElements.first { it.id !in usedIds[diffRow.oldLine].orEmpty() } + } + + fun addToUsedIds(id: String) { + val currentIds = usedIds[diffRow.oldLine] ?: mutableListOf() + usedIds[diffRow.oldLine] = currentIds.apply { add(id) } } return when(diffRow.tag) { DiffRow.Tag.EQUAL -> { - val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) - val currentIds = usedIds[diffRow.oldLine] ?: mutableListOf() - usedIds[diffRow.oldLine] = currentIds.apply { add(jsonTreeElement.id) } + val jsonTreeElement = findJsonTreeElement() + addToUsedIds(id = jsonTreeElement.id) JsonDiffElement.Equal(jsonTreeElement) } DiffRow.Tag.CHANGE -> { - val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) - val currentIds = usedIds[diffRow.oldLine] ?: mutableListOf() - usedIds[diffRow.oldLine] = currentIds.apply { add(jsonTreeElement.id) } + val jsonTreeElement = findJsonTreeElement() + addToUsedIds(id = jsonTreeElement.id) JsonDiffElement.Change( jsonTreeElement = jsonTreeElement, inlineDiffIndices = inlineDiffsIndices @@ -175,9 +162,8 @@ internal class JsonTreeDiffer( JsonDiffElement.Insertion(null) } DiffRow.Tag.DELETE -> { - val jsonTreeElement = findJsonTreeElement(diffRow.oldLine) - val currentIds = usedIds[diffRow.oldLine] ?: mutableListOf() - usedIds[diffRow.oldLine] = currentIds.apply { add(jsonTreeElement.id) } + val jsonTreeElement = findJsonTreeElement() + addToUsedIds(id = jsonTreeElement.id) JsonDiffElement.Deletion(jsonTreeElement) } } @@ -189,31 +175,33 @@ internal class JsonTreeDiffer( revisedJsonTreeMap: Map>, inlineDiffsIndices: List> ): JsonDiffElement { - fun findJsonTreeElement(diffLine: String): JsonTreeElement { - val jsonTreeElements = revisedJsonTreeMap.getValue(diffLine) - return jsonTreeElements.first { it.id !in usedIds[diffLine].orEmpty() } + fun findJsonTreeElement(): JsonTreeElement { + val jsonTreeElements = revisedJsonTreeMap.getValue(diffRow.newLine) + return jsonTreeElements.first { it.id !in usedIds[diffRow.newLine].orEmpty() } + } + + fun addToUsedIds(id: String) { + val currentIds = usedIds[diffRow.newLine] ?: mutableListOf() + usedIds[diffRow.newLine] = currentIds.apply { add(id) } } return when(diffRow.tag) { DiffRow.Tag.EQUAL -> { - val jsonTreeElement = findJsonTreeElement(diffRow.newLine) - val currentIds = usedIds[diffRow.newLine] ?: mutableListOf() - usedIds[diffRow.newLine] = currentIds.apply { add(jsonTreeElement.id) } + val jsonTreeElement = findJsonTreeElement() + addToUsedIds(id = jsonTreeElement.id) JsonDiffElement.Equal(jsonTreeElement) } DiffRow.Tag.CHANGE -> { - val jsonTreeElement = findJsonTreeElement(diffRow.newLine) - val currentIds = usedIds[diffRow.newLine] ?: mutableListOf() - usedIds[diffRow.newLine] = currentIds.apply { add(jsonTreeElement.id) } + val jsonTreeElement = findJsonTreeElement() + addToUsedIds(id = jsonTreeElement.id) JsonDiffElement.Change( jsonTreeElement = jsonTreeElement, inlineDiffIndices = inlineDiffsIndices ) } DiffRow.Tag.INSERT -> { - val jsonTreeElement = findJsonTreeElement(diffRow.newLine) - val currentIds = usedIds[diffRow.newLine] ?: mutableListOf() - usedIds[diffRow.newLine] = currentIds.apply { add(jsonTreeElement.id) } + val jsonTreeElement = findJsonTreeElement() + addToUsedIds(id = jsonTreeElement.id) JsonDiffElement.Insertion(jsonTreeElement) } DiffRow.Tag.DELETE -> JsonDiffElement.Deletion(null) @@ -262,6 +250,13 @@ internal class JsonTreeDiffer( return result } + private fun CoroutineScope.runAsync(block: () -> T): Deferred = async { + suspendCancellableCoroutine { continuation -> + val result = block() + continuation.resume(result) + } + } + private val inlineDiffTagOpen = "JSON_TREE_DIFF_START_TAG" private val inlineDiffTagClosed = "JSON_TREE_DIFF_END_TAG" } From f30912562c034fd0f2122df33a0950b6a901415b Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Tue, 17 Mar 2026 15:05:53 +0000 Subject: [PATCH 16/32] Use fast functions --- .../jsontree/diff/JsonTreeDiffer.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index b2e1855..16f5b46 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -1,5 +1,7 @@ package com.sebastianneubauer.jsontree.diff +import androidx.compose.ui.util.fastFirst +import androidx.compose.ui.util.fastMap import com.sebastianneubauer.jsontree.JsonTreeElement import com.sebastianneubauer.jsontree.JsonTreeParser import com.sebastianneubauer.jsontree.JsonTreeParserState @@ -19,6 +21,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlin.collections.orEmpty import kotlin.coroutines.resume +import kotlin.time.Clock internal class JsonTreeDiffer( val defaultDispatcher: CoroutineDispatcher, @@ -76,8 +79,8 @@ internal class JsonTreeDiffer( newTag = diffTagGenerator, oldTag = diffTagGenerator, ).generateDiffRows( - originalJsonTreeList.map { it.toRenderString() }, - revisedJsonTreeList.map { it.toRenderString() } + originalJsonTreeList.fastMap { it.toRenderString() }, + revisedJsonTreeList.fastMap { it.toRenderString() } ) val originalJsonTreeMap = originalJsonTreeMapDeferred.await() @@ -86,7 +89,7 @@ internal class JsonTreeDiffer( val usedOriginalIds = mutableMapOf>() val usedRevisedIds = mutableMapOf>() - val diffElements = diffRows.map { diffRow -> + val diffElements = diffRows.fastMap { diffRow -> val (oldLineDiffIndices, newLineDiffIndices) = if(diffRow.tag == DiffRow.Tag.CHANGE) { val oldLineIndices = diffRow.oldLine.findInlineDiffTagIndices() val newLineIndices = diffRow.newLine.findInlineDiffTagIndices() @@ -136,7 +139,7 @@ internal class JsonTreeDiffer( ): JsonDiffElement { fun findJsonTreeElement(): JsonTreeElement { val jsonTreeElements = originalJsonTreeMap.getValue(diffRow.oldLine) - return jsonTreeElements.first { it.id !in usedIds[diffRow.oldLine].orEmpty() } + return jsonTreeElements.fastFirst { it.id !in usedIds[diffRow.oldLine].orEmpty() } } fun addToUsedIds(id: String) { @@ -177,7 +180,7 @@ internal class JsonTreeDiffer( ): JsonDiffElement { fun findJsonTreeElement(): JsonTreeElement { val jsonTreeElements = revisedJsonTreeMap.getValue(diffRow.newLine) - return jsonTreeElements.first { it.id !in usedIds[diffRow.newLine].orEmpty() } + return jsonTreeElements.fastFirst { it.id !in usedIds[diffRow.newLine].orEmpty() } } fun addToUsedIds(id: String) { From a0538c5efa63b6deb84d812a3b25454f06d9af0f Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Tue, 17 Mar 2026 16:07:46 +0000 Subject: [PATCH 17/32] Don't depend on JsonTreeParser in JsonTreeDiff --- .../jsontree/JsonTreeParser.kt | 91 +------------------ .../jsontree/diff/JsonTreeDiff.kt | 5 +- .../jsontree/diff/JsonTreeDiffer.kt | 63 ++++++------- .../jsontree/util/IdGenerator.kt | 10 ++ .../util/JsonTreeElementExtensions.kt | 82 +++++++++++++++++ 5 files changed, 131 insertions(+), 120 deletions(-) create mode 100644 jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/IdGenerator.kt diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt index fbe5c44..be2c2d6 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt @@ -11,20 +11,15 @@ import com.sebastianneubauer.jsontree.JsonTreeParserState.Loading import com.sebastianneubauer.jsontree.JsonTreeParserState.Parsing.Error import com.sebastianneubauer.jsontree.JsonTreeParserState.Parsing.Parsed import com.sebastianneubauer.jsontree.JsonTreeParserState.Ready +import com.sebastianneubauer.jsontree.util.IdGenerator import com.sebastianneubauer.jsontree.util.Expansion import com.sebastianneubauer.jsontree.util.collapse import com.sebastianneubauer.jsontree.util.expand +import com.sebastianneubauer.jsontree.util.toJsonTreeElement import com.sebastianneubauer.jsontree.util.toList -import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject internal class JsonTreeParser( private val json: String, @@ -46,7 +41,7 @@ internal class JsonTreeParser( Ready( list = parsingState.jsonElement .toJsonTreeElement( - idGenerator = AtomicLongWrapper(), + idGenerator = IdGenerator(), state = initialState, level = 0, key = null, @@ -133,84 +128,4 @@ internal class JsonTreeParser( addAll(itemIndex, newItems) } } - - private fun JsonElement.toJsonTreeElement( - idGenerator: AtomicLongWrapper, - state: TreeState, - level: Int, - key: String?, - isLastItem: Boolean, - parentType: ParentType, - ): JsonTreeElement { - return when (this) { - is JsonPrimitive -> { - Primitive( - id = idGenerator.incrementAndGet().toString(), - level = level, - key = key, - value = this, - isLastItem = isLastItem, - parentType = parentType, - ) - } - is JsonArray -> { - val childElements = jsonArray.mapIndexed { index, item -> - Pair( - index.toString(), - item.toJsonTreeElement( - idGenerator = idGenerator, - state = if (state == TreeState.FIRST_ITEM_EXPANDED) TreeState.COLLAPSED else state, - level = level + 1, - key = index.toString(), - isLastItem = index == (jsonArray.size - 1), - parentType = ParentType.ARRAY, - ) - ) - } - .toMap() - - Array( - id = idGenerator.incrementAndGet().toString(), - level = level, - state = state, - key = key, - children = childElements, - isLastItem = isLastItem, - parentType = parentType, - ) - } - is JsonObject -> { - val childElements = jsonObject.entries.associate { - Pair( - it.key, - it.value.toJsonTreeElement( - idGenerator = idGenerator, - state = if (state == TreeState.FIRST_ITEM_EXPANDED) TreeState.COLLAPSED else state, - level = level + 1, - key = it.key, - isLastItem = it == jsonObject.entries.last(), - parentType = ParentType.OBJECT - ) - ) - } - - Object( - id = idGenerator.incrementAndGet().toString(), - level = level, - state = state, - key = key, - children = childElements, - isLastItem = isLastItem, - parentType = parentType, - ) - } - } - } -} - -internal class AtomicLongWrapper { - private val atomicLong = atomic(0L) - fun incrementAndGet(): Long { - return atomicLong.incrementAndGet() - } } diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index ca16737..ed21714 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -85,7 +85,10 @@ private fun SideBySideDiff( LazyColumn( modifier = Modifier.fillMaxWidth(), ) { - itemsIndexed(state.diffElements) { index, (originalDiffElement, revisedDiffElement) -> + itemsIndexed( + items = state.diffElements, + key = { index, _ -> index } + ) { index, (originalDiffElement, revisedDiffElement) -> val originalJsonTreeElement = when(originalDiffElement) { is JsonDiffElement.Change -> originalDiffElement.jsonTreeElement is JsonDiffElement.Deletion -> originalDiffElement.jsonTreeElement!! diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index 16f5b46..3944f24 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -3,25 +3,23 @@ package com.sebastianneubauer.jsontree.diff import androidx.compose.ui.util.fastFirst import androidx.compose.ui.util.fastMap import com.sebastianneubauer.jsontree.JsonTreeElement -import com.sebastianneubauer.jsontree.JsonTreeParser -import com.sebastianneubauer.jsontree.JsonTreeParserState +import com.sebastianneubauer.jsontree.JsonTreeElement.ParentType import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement +import com.sebastianneubauer.jsontree.util.IdGenerator +import com.sebastianneubauer.jsontree.util.toJsonTreeElement +import com.sebastianneubauer.jsontree.util.toList import com.sebastianneubauer.jsontree.util.toRenderString import io.github.petertrr.diffutils.text.DiffLineNormalizer import io.github.petertrr.diffutils.text.DiffRow import io.github.petertrr.diffutils.text.DiffRowGenerator import io.github.petertrr.diffutils.text.DiffTagGenerator import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import kotlin.collections.orEmpty -import kotlin.coroutines.resume -import kotlin.time.Clock internal class JsonTreeDiffer( val defaultDispatcher: CoroutineDispatcher, @@ -47,25 +45,25 @@ internal class JsonTreeDiffer( val revisedJsonTreeListResult = revisedJsonTreeListDeferred.await() val (originalJsonTreeList, revisedJsonTreeList) = when { - originalJsonTreeListResult is JsonTreeParserState.Parsing.Error -> { + originalJsonTreeListResult is ParsingResult.Failure -> { withContext(mainDispatcher) { state.value = JsonTreeDifferState.Error.OriginalJsonError(originalJsonTreeListResult.throwable) } return@withContext } - revisedJsonTreeListResult is JsonTreeParserState.Parsing.Error -> { + revisedJsonTreeListResult is ParsingResult.Failure -> { withContext(mainDispatcher) { state.value = JsonTreeDifferState.Error.RevisedJsonError(revisedJsonTreeListResult.throwable) } return@withContext } else -> { - (originalJsonTreeListResult as JsonTreeParserState.Ready).list to (revisedJsonTreeListResult as JsonTreeParserState.Ready).list + (originalJsonTreeListResult as ParsingResult.Success).list to (revisedJsonTreeListResult as ParsingResult.Success).list } } - val originalJsonTreeMapDeferred = runAsync { originalJsonTreeList.groupBy { it.toRenderString() } } - val revisedJsonTreeMapDeferred = runAsync { revisedJsonTreeList.groupBy { it.toRenderString() } } + val originalJsonTreeMapDeferred = async { originalJsonTreeList.groupBy { it.toRenderString() } } + val revisedJsonTreeMapDeferred = async { revisedJsonTreeList.groupBy { it.toRenderString() } } val diffTagGenerator = object : DiffTagGenerator { override fun generateClose(tag: DiffRow.Tag): String = inlineDiffTagClosed @@ -103,7 +101,7 @@ internal class JsonTreeDiffer( newLine = diffRow.newLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, "") ) - val originalDiffElementDeferred = runAsync { + val originalDiffElementDeferred = async { getOriginalDiffElement( diffRow = strippedTagDiffRow, usedIds = usedOriginalIds, @@ -112,7 +110,7 @@ internal class JsonTreeDiffer( ) } - val revisedDiffElementDeferred = runAsync { + val revisedDiffElementDeferred = async { getRevisedDiffElement( diffRow = strippedTagDiffRow, usedIds = usedRevisedIds, @@ -211,18 +209,23 @@ internal class JsonTreeDiffer( } } - private suspend fun getJsonTreeList(json: String): JsonTreeParserState { - val originalParser = JsonTreeParser( - json = json, - defaultDispatcher = defaultDispatcher, - mainDispatcher = mainDispatcher - ).also { it.init(TreeState.EXPANDED) } - - return when(val state = originalParser.state.value) { - is JsonTreeParserState.Ready -> state - is JsonTreeParserState.Parsing.Error -> state - is JsonTreeParserState.Loading, - is JsonTreeParserState.Parsing.Parsed -> error("Impossible state $state!") + private fun getJsonTreeList(json: String): ParsingResult { + return runCatching { + ParsingResult.Success( + Json + .parseToJsonElement(json) + .toJsonTreeElement( + idGenerator = IdGenerator(), + state = TreeState.EXPANDED, + level = 0, + key = null, + isLastItem = true, + parentType = ParentType.NONE + ) + .toList() + ) + }.getOrElse { + ParsingResult.Failure(throwable = it) } } @@ -253,11 +256,9 @@ internal class JsonTreeDiffer( return result } - private fun CoroutineScope.runAsync(block: () -> T): Deferred = async { - suspendCancellableCoroutine { continuation -> - val result = block() - continuation.resume(result) - } + private sealed interface ParsingResult { + data class Success(val list: List): ParsingResult + data class Failure(val throwable: Throwable): ParsingResult } private val inlineDiffTagOpen = "JSON_TREE_DIFF_START_TAG" diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/IdGenerator.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/IdGenerator.kt new file mode 100644 index 0000000..121a3d7 --- /dev/null +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/IdGenerator.kt @@ -0,0 +1,10 @@ +package com.sebastianneubauer.jsontree.util + +import kotlinx.atomicfu.atomic + +internal class IdGenerator { + private val atomicLong = atomic(0L) + fun incrementAndGet(): Long { + return atomicLong.incrementAndGet() + } +} \ No newline at end of file diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/JsonTreeElementExtensions.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/JsonTreeElementExtensions.kt index 045024e..388e396 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/JsonTreeElementExtensions.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/JsonTreeElementExtensions.kt @@ -8,6 +8,12 @@ import com.sebastianneubauer.jsontree.JsonTreeElement.Primitive import com.sebastianneubauer.jsontree.JsonTreeElement.ParentType import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.endBracket +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject internal enum class Expansion { /** @@ -181,6 +187,82 @@ internal fun JsonTreeElement.toList(): List { return list } +/** + * Converts a [JsonElement] to a [JsonTreeElement]. + */ +internal fun JsonElement.toJsonTreeElement( + idGenerator: IdGenerator, + state: TreeState, + level: Int, + key: String?, + isLastItem: Boolean, + parentType: ParentType, +): JsonTreeElement { + return when (this) { + is JsonPrimitive -> { + Primitive( + id = idGenerator.incrementAndGet().toString(), + level = level, + key = key, + value = this, + isLastItem = isLastItem, + parentType = parentType, + ) + } + is JsonArray -> { + val childElements = jsonArray.mapIndexed { index, item -> + Pair( + index.toString(), + item.toJsonTreeElement( + idGenerator = idGenerator, + state = if (state == TreeState.FIRST_ITEM_EXPANDED) TreeState.COLLAPSED else state, + level = level + 1, + key = index.toString(), + isLastItem = index == (jsonArray.size - 1), + parentType = ParentType.ARRAY, + ) + ) + } + .toMap() + + Array( + id = idGenerator.incrementAndGet().toString(), + level = level, + state = state, + key = key, + children = childElements, + isLastItem = isLastItem, + parentType = parentType, + ) + } + is JsonObject -> { + val childElements = jsonObject.entries.associate { + Pair( + it.key, + it.value.toJsonTreeElement( + idGenerator = idGenerator, + state = if (state == TreeState.FIRST_ITEM_EXPANDED) TreeState.COLLAPSED else state, + level = level + 1, + key = it.key, + isLastItem = it == jsonObject.entries.last(), + parentType = ParentType.OBJECT + ) + ) + } + + Object( + id = idGenerator.incrementAndGet().toString(), + level = level, + state = state, + key = key, + children = childElements, + isLastItem = isLastItem, + parentType = parentType, + ) + } + } +} + /** * Converts a JsonTreeElement to its string representation. */ From fe592bc081f9aef47a9b2820c42dd7db43ca25d8 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sat, 21 Mar 2026 16:24:36 +0000 Subject: [PATCH 18/32] Add onStateChanged to API and update sample --- .../jsontree/diff/JsonTreeDiff.kt | 26 ++--- .../jsontree/diff/JsonTreeDiffError.kt | 14 --- .../jsontree/diff/JsonTreeDiffState.kt | 62 +++++++++++ .../jsontree/diff/JsonTreeDiffer.kt | 22 +++- .../jsontree/diff/JsonTreeDifferState.kt | 3 +- sample/src/commonMain/kotlin/App.kt | 103 ++++++++++++------ 6 files changed, 166 insertions(+), 64 deletions(-) delete mode 100644 jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffError.kt create mode 100644 jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index ed21714..dffc253 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -4,28 +4,24 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.sebastianneubauer.jsontree.CollapsableType @@ -34,18 +30,17 @@ import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement import com.sebastianneubauer.jsontree.diff.JsonTreeDiffError.OriginalJsonError import com.sebastianneubauer.jsontree.diff.JsonTreeDiffError.RevisedJsonError import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable public fun JsonTreeDiff( originalJson: String, revisedJson: String, - onLoading: @Composable () -> Unit, + onStateChanged: (JsonTreeDiffState) -> Unit, modifier: Modifier = Modifier, showInlineDiffs: Boolean = true, + contentPadding: PaddingValues = PaddingValues(0.dp), colors: JsonTreeDiffColors = defaultLightDiffColors, - textStyle: TextStyle = LocalTextStyle.current, - onError: (JsonTreeDiffError) -> Unit = {} + textStyle: TextStyle = LocalTextStyle.current ) { val jsonTreeDiffer = remember { JsonTreeDiffer( @@ -63,16 +58,19 @@ public fun JsonTreeDiff( } when(val state = jsonTreeDiffer.state.collectAsState().value) { - is JsonTreeDifferState.Loading -> onLoading() + is JsonTreeDifferState.Loading -> onStateChanged(JsonTreeDiffState.Loading) is JsonTreeDifferState.Ready -> Box(modifier = modifier) { + onStateChanged(JsonTreeDiffState.Success(state.diffInfo)) + SideBySideDiff( state = state, colors = colors, textStyle = textStyle, + contentPadding = contentPadding ) } - is JsonTreeDifferState.Error.OriginalJsonError -> onError(OriginalJsonError(state.throwable)) - is JsonTreeDifferState.Error.RevisedJsonError -> onError(RevisedJsonError(state.throwable)) + is JsonTreeDifferState.Error.OriginalJsonError -> onStateChanged(JsonTreeDiffState.Error(OriginalJsonError(state.throwable))) + is JsonTreeDifferState.Error.RevisedJsonError -> onStateChanged(JsonTreeDiffState.Error(RevisedJsonError(state.throwable))) } } @@ -81,9 +79,11 @@ private fun SideBySideDiff( state: JsonTreeDifferState.Ready, colors: JsonTreeDiffColors, textStyle: TextStyle, + contentPadding: PaddingValues, ) { LazyColumn( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.background(color = colors.regularBackgroundColor), + contentPadding = contentPadding ) { itemsIndexed( items = state.diffElements, diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffError.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffError.kt deleted file mode 100644 index e984e89..0000000 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffError.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.sebastianneubauer.jsontree.diff - -public interface JsonTreeDiffError { - public val throwable: Throwable - /** - * Describes an error during parsing of the original Json. - */ - public class OriginalJsonError(override val throwable: Throwable): JsonTreeDiffError - - /** - * Describes an error during parsing of the revised Json. - */ - public class RevisedJsonError(override val throwable: Throwable): JsonTreeDiffError -} \ No newline at end of file diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt new file mode 100644 index 0000000..d3e2002 --- /dev/null +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt @@ -0,0 +1,62 @@ +package com.sebastianneubauer.jsontree.diff + +/** + * Describes the current state of the diff calculation. + */ +public sealed interface JsonTreeDiffState { + /** + * The diff is currently being calculated. + */ + public data object Loading: JsonTreeDiffState + + /** + * The diff calculation succeeded. See [info] for more details on the diff. + */ + public data class Success(val info: JsonTreeDiffInfo): JsonTreeDiffState + + /** + * The diff calculation failed with an error. See [error] for details. + */ + public data class Error(val error: JsonTreeDiffError): JsonTreeDiffState +} + +/** + * Infos about the calculated diff. + */ +public data class JsonTreeDiffInfo( + val changeInfo: ChangeInfo +) + +public sealed interface ChangeInfo { + /** + * The given Json strings are identical. + */ + public data object Identical: ChangeInfo + + /** + * The given Json strings have differences. + */ + public data class Changed( + /** + * The amount of inserted lines in the revised Json. + */ + val insertions: Int, + /** + * The amount of deleted lines in the original Json. + */ + val deletions: Int, + ): ChangeInfo +} + +public interface JsonTreeDiffError { + public val throwable: Throwable + /** + * Describes an error during parsing of the original Json. + */ + public class OriginalJsonError(override val throwable: Throwable): JsonTreeDiffError + + /** + * Describes an error during parsing of the revised Json. + */ + public class RevisedJsonError(override val throwable: Throwable): JsonTreeDiffError +} \ No newline at end of file diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index 3944f24..a61da58 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -81,6 +81,23 @@ internal class JsonTreeDiffer( revisedJsonTreeList.fastMap { it.toRenderString() } ) + val diffInfoDeferred = async { + // A change is both a deletion in the original json and a insertion in the revised json + val insertions = diffRows.count { it.tag == DiffRow.Tag.INSERT || it.tag == DiffRow.Tag.CHANGE } + val deletions = diffRows.count { it.tag == DiffRow.Tag.DELETE || it.tag == DiffRow.Tag.CHANGE } + + JsonTreeDiffInfo( + changeInfo = if(insertions == 0 && deletions == 0) { + ChangeInfo.Identical + } else { + ChangeInfo.Changed( + insertions = insertions, + deletions = deletions + ) + } + ) + } + val originalJsonTreeMap = originalJsonTreeMapDeferred.await() val revisedJsonTreeMap = revisedJsonTreeMapDeferred.await() @@ -122,9 +139,12 @@ internal class JsonTreeDiffer( Pair(originalDiffElementDeferred.await(), revisedDiffElementDeferred.await()) } + val diffInfo = diffInfoDeferred.await() + withContext(mainDispatcher) { state.value = JsonTreeDifferState.Ready( - diffElements = diffElements + diffElements = diffElements, + diffInfo = diffInfo ) } } diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt index c096efc..5918a9c 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt @@ -10,7 +10,8 @@ internal sealed interface JsonTreeDifferState { data object Loading: JsonTreeDifferState data class Ready( - val diffElements: List> + val diffElements: List>, + val diffInfo: JsonTreeDiffInfo ): JsonTreeDifferState @Immutable diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt index 31a5a05..70005dd 100644 --- a/sample/src/commonMain/kotlin/App.kt +++ b/sample/src/commonMain/kotlin/App.kt @@ -50,8 +50,10 @@ import com.sebastianneubauer.jsontree.TreeColors import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.defaultDarkColors import com.sebastianneubauer.jsontree.defaultLightColors +import com.sebastianneubauer.jsontree.diff.ChangeInfo import com.sebastianneubauer.jsontree.diff.JsonTreeDiff import com.sebastianneubauer.jsontree.diff.JsonTreeDiffError +import com.sebastianneubauer.jsontree.diff.JsonTreeDiffState import com.sebastianneubauer.jsontree.diff.defaultDarkDiffColors import com.sebastianneubauer.jsontree.diff.defaultLightDiffColors import com.sebastianneubauer.jsontree.search.rememberSearchState @@ -367,51 +369,82 @@ private fun JsonDiff( showInlineDiffs: Boolean, colors: TreeColors ) { - var error by remember(originalJson) { mutableStateOf(null) } - val errorMessage = error + var state by remember(originalJson) { mutableStateOf(null) } + val currentState = state + + Column { + val lineChanges = if(currentState is JsonTreeDiffState.Success) { + when(val changeInfo = currentState.info.changeInfo) { + is ChangeInfo.Identical -> "Identical" + is ChangeInfo.Changed -> "+${changeInfo.insertions} -${changeInfo.deletions}" + } + } else null + + Row { + Text( + modifier = Modifier.weight(1F), + text = "Original:", + color = if (colors == defaultLightColors) Color.Black else Color.White + ) + + Row(modifier = Modifier.weight(1F)) { + Text( + modifier = Modifier.weight(1F), + text = "Revised:", + color = if (colors == defaultLightColors) Color.Black else Color.White + ) + + if(lineChanges != null) { + Text( + text = lineChanges, + color = if (colors == defaultLightColors) Color.Black else Color.White + ) + } + } + } - if (errorMessage != null) { - Text( - modifier = Modifier - .fillMaxSize() - .background( - color = if (colors == defaultLightColors) Color.White else Color.Black - ), - text = errorMessage, - color = if (colors == defaultLightColors) Color.Black else Color.White, - ) - } else { JsonTreeDiff( modifier = Modifier .fillMaxWidth() - .height(400.dp) - .background( - if (colors == defaultLightColors) Color.White else Color.Black - ), + .height(400.dp), originalJson = originalJson, revisedJson = complexJsonRevised, showInlineDiffs = showInlineDiffs, - onLoading = { - Box( - modifier = Modifier - .fillMaxSize() - .background( - if (colors == defaultLightColors) Color.White else Color.Black - ), - contentAlignment = Alignment.Center - ) { - Text( - text = "Loading...", - color = if (colors == defaultLightColors) Color.Black else Color.White - ) - } - }, colors = if(colors == defaultLightColors) defaultLightDiffColors else defaultDarkDiffColors, - onError = { - error = it.throwable.message - } + onStateChanged = { state = it }, ) } + + when(currentState) { + is JsonTreeDiffState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .background( + if (colors == defaultLightColors) Color.White else Color.Black + ), + contentAlignment = Alignment.Center + ) { + Text( + text = "Loading...", + color = if (colors == defaultLightColors) Color.Black else Color.White + ) + } + } + is JsonTreeDiffState.Error -> { + Text( + modifier = Modifier + .fillMaxSize() + .background( + color = if (colors == defaultLightColors) Color.White else Color.Black + ), + text = currentState.error.throwable.message ?: "Unknown error", + color = if (colors == defaultLightColors) Color.Black else Color.White, + ) + } + is JsonTreeDiffState.Success, + null -> Unit + } } @Preview From 3b3c984a30e5593e0b52b040a76793c9edf145bc Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sat, 4 Apr 2026 10:04:08 +0000 Subject: [PATCH 19/32] Adjust API and fix recomposition --- .../jsontree/diff/JsonTreeDiff.kt | 36 +++++---- .../jsontree/diff/JsonTreeDiffState.kt | 46 +++-------- .../jsontree/diff/JsonTreeDiffer.kt | 35 ++++---- .../jsontree/diff/JsonTreeDifferTest.kt | 22 ++--- sample/src/commonMain/kotlin/App.kt | 80 +++++++++---------- 5 files changed, 96 insertions(+), 123 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index dffc253..665c1c6 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -35,21 +34,24 @@ import kotlinx.coroutines.Dispatchers public fun JsonTreeDiff( originalJson: String, revisedJson: String, - onStateChanged: (JsonTreeDiffState) -> Unit, + onLoading: @Composable () -> Unit, + onError: @Composable (Error) -> Unit, + onSuccess: (Success) -> Unit= {}, modifier: Modifier = Modifier, showInlineDiffs: Boolean = true, contentPadding: PaddingValues = PaddingValues(0.dp), colors: JsonTreeDiffColors = defaultLightDiffColors, textStyle: TextStyle = LocalTextStyle.current ) { - val jsonTreeDiffer = remember { + // resets the internal state to avoid rendering an outdated state and calling onSuccess, until the LaunchedEffect is called + val jsonTreeDiffer = remember(originalJson, revisedJson, showInlineDiffs) { JsonTreeDiffer( defaultDispatcher = Dispatchers.Default, mainDispatcher = Dispatchers.Main ) } - LaunchedEffect(originalJson, revisedJson, showInlineDiffs) { + LaunchedEffect(jsonTreeDiffer) { jsonTreeDiffer.diff( original = originalJson, revised = revisedJson, @@ -57,20 +59,22 @@ public fun JsonTreeDiff( ) } - when(val state = jsonTreeDiffer.state.collectAsState().value) { - is JsonTreeDifferState.Loading -> onStateChanged(JsonTreeDiffState.Loading) - is JsonTreeDifferState.Ready -> Box(modifier = modifier) { - onStateChanged(JsonTreeDiffState.Success(state.diffInfo)) + when(val state = jsonTreeDiffer.state.value) { + is JsonTreeDifferState.Loading -> onLoading() + is JsonTreeDifferState.Ready -> { + onSuccess(Success(state.diffInfo)) - SideBySideDiff( - state = state, - colors = colors, - textStyle = textStyle, - contentPadding = contentPadding - ) + Box(modifier = modifier) { + SideBySideDiff( + state = state, + colors = colors, + textStyle = textStyle, + contentPadding = contentPadding + ) + } } - is JsonTreeDifferState.Error.OriginalJsonError -> onStateChanged(JsonTreeDiffState.Error(OriginalJsonError(state.throwable))) - is JsonTreeDifferState.Error.RevisedJsonError -> onStateChanged(JsonTreeDiffState.Error(RevisedJsonError(state.throwable))) + is JsonTreeDifferState.Error.OriginalJsonError -> onError(Error(OriginalJsonError(state.throwable))) + is JsonTreeDifferState.Error.RevisedJsonError -> onError(Error(RevisedJsonError(state.throwable))) } } diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt index d3e2002..79f1aff 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt @@ -1,24 +1,9 @@ package com.sebastianneubauer.jsontree.diff /** - * Describes the current state of the diff calculation. + * The diff calculation succeeded. See [info] for more details on the diff. */ -public sealed interface JsonTreeDiffState { - /** - * The diff is currently being calculated. - */ - public data object Loading: JsonTreeDiffState - - /** - * The diff calculation succeeded. See [info] for more details on the diff. - */ - public data class Success(val info: JsonTreeDiffInfo): JsonTreeDiffState - - /** - * The diff calculation failed with an error. See [error] for details. - */ - public data class Error(val error: JsonTreeDiffError): JsonTreeDiffState -} +public data class Success(val info: JsonTreeDiffInfo) /** * Infos about the calculated diff. @@ -27,26 +12,21 @@ public data class JsonTreeDiffInfo( val changeInfo: ChangeInfo ) -public sealed interface ChangeInfo { +public data class ChangeInfo( /** - * The given Json strings are identical. + * The amount of inserted lines in the revised Json. */ - public data object Identical: ChangeInfo - + val insertions: Int, /** - * The given Json strings have differences. + * The amount of deleted lines in the original Json. */ - public data class Changed( - /** - * The amount of inserted lines in the revised Json. - */ - val insertions: Int, - /** - * The amount of deleted lines in the original Json. - */ - val deletions: Int, - ): ChangeInfo -} + val deletions: Int, +) + +/** + * The diff calculation failed with an error. See [error] for details. + */ +public data class Error(val error: JsonTreeDiffError) public interface JsonTreeDiffError { public val throwable: Throwable diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index a61da58..d63c101 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -1,11 +1,16 @@ package com.sebastianneubauer.jsontree.diff +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.util.fastFirst import androidx.compose.ui.util.fastMap import com.sebastianneubauer.jsontree.JsonTreeElement import com.sebastianneubauer.jsontree.JsonTreeElement.ParentType import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement +import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.Loading +import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.Ready +import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.Error import com.sebastianneubauer.jsontree.util.IdGenerator import com.sebastianneubauer.jsontree.util.toJsonTreeElement import com.sebastianneubauer.jsontree.util.toList @@ -16,7 +21,6 @@ import io.github.petertrr.diffutils.text.DiffRowGenerator import io.github.petertrr.diffutils.text.DiffTagGenerator import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.async -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlin.collections.orEmpty @@ -25,19 +29,14 @@ internal class JsonTreeDiffer( val defaultDispatcher: CoroutineDispatcher, val mainDispatcher: CoroutineDispatcher ) { - val state = MutableStateFlow(JsonTreeDifferState.Loading) + private val differState = mutableStateOf(Loading) + val state: State = differState suspend fun diff( original: String, revised: String, showInlineDiffs: Boolean, ) = withContext(defaultDispatcher) { - if(state.value !is JsonTreeDifferState.Loading) { - withContext(mainDispatcher) { - state.value = JsonTreeDifferState.Loading - } - } - val originalJsonTreeListDeferred = async { getJsonTreeList(original) } val revisedJsonTreeListDeferred = async { getJsonTreeList(revised) } @@ -47,13 +46,13 @@ internal class JsonTreeDiffer( val (originalJsonTreeList, revisedJsonTreeList) = when { originalJsonTreeListResult is ParsingResult.Failure -> { withContext(mainDispatcher) { - state.value = JsonTreeDifferState.Error.OriginalJsonError(originalJsonTreeListResult.throwable) + differState.value = Error.OriginalJsonError(originalJsonTreeListResult.throwable) } return@withContext } revisedJsonTreeListResult is ParsingResult.Failure -> { withContext(mainDispatcher) { - state.value = JsonTreeDifferState.Error.RevisedJsonError(revisedJsonTreeListResult.throwable) + differState.value = Error.RevisedJsonError(revisedJsonTreeListResult.throwable) } return@withContext } @@ -87,14 +86,10 @@ internal class JsonTreeDiffer( val deletions = diffRows.count { it.tag == DiffRow.Tag.DELETE || it.tag == DiffRow.Tag.CHANGE } JsonTreeDiffInfo( - changeInfo = if(insertions == 0 && deletions == 0) { - ChangeInfo.Identical - } else { - ChangeInfo.Changed( - insertions = insertions, - deletions = deletions - ) - } + changeInfo = ChangeInfo( + insertions = insertions, + deletions = deletions + ) ) } @@ -105,7 +100,7 @@ internal class JsonTreeDiffer( val usedRevisedIds = mutableMapOf>() val diffElements = diffRows.fastMap { diffRow -> - val (oldLineDiffIndices, newLineDiffIndices) = if(diffRow.tag == DiffRow.Tag.CHANGE) { + val (oldLineDiffIndices, newLineDiffIndices) = if(diffRow.tag == DiffRow.Tag.CHANGE && showInlineDiffs) { val oldLineIndices = diffRow.oldLine.findInlineDiffTagIndices() val newLineIndices = diffRow.newLine.findInlineDiffTagIndices() Pair(oldLineIndices, newLineIndices) @@ -142,7 +137,7 @@ internal class JsonTreeDiffer( val diffInfo = diffInfoDeferred.await() withContext(mainDispatcher) { - state.value = JsonTreeDifferState.Ready( + differState.value = Ready( diffElements = diffElements, diffInfo = diffInfo ) diff --git a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt index 8f7a334..43f19a1 100644 --- a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt +++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt @@ -190,7 +190,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.state.value + actual = differ.differState.value ) } @@ -291,7 +291,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.state.value + actual = differ.differState.value ) } @@ -392,7 +392,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.state.value + actual = differ.differState.value ) } @@ -483,7 +483,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.state.value + actual = differ.differState.value ) } @@ -497,7 +497,7 @@ public class JsonTreeDifferTest { showInlineDiffs = false ) - val state = differ.state.value + val state = differ.differState.value assertTrue(state is JsonTreeDifferState.Error.OriginalJsonError) } @@ -511,7 +511,7 @@ public class JsonTreeDifferTest { showInlineDiffs = false ) - val state = differ.state.value + val state = differ.differState.value assertTrue(state is JsonTreeDifferState.Error.RevisedJsonError) } @@ -608,7 +608,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.state.value + actual = differ.differState.value ) } @@ -741,7 +741,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.state.value + actual = differ.differState.value ) } @@ -870,7 +870,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.state.value + actual = differ.differState.value ) } @@ -1013,7 +1013,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.state.value + actual = differ.differState.value ) } @@ -1156,7 +1156,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.state.value + actual = differ.differState.value ) } } \ No newline at end of file diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt index 70005dd..2ddc406 100644 --- a/sample/src/commonMain/kotlin/App.kt +++ b/sample/src/commonMain/kotlin/App.kt @@ -30,7 +30,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextField -import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -50,10 +49,8 @@ import com.sebastianneubauer.jsontree.TreeColors import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.defaultDarkColors import com.sebastianneubauer.jsontree.defaultLightColors -import com.sebastianneubauer.jsontree.diff.ChangeInfo import com.sebastianneubauer.jsontree.diff.JsonTreeDiff -import com.sebastianneubauer.jsontree.diff.JsonTreeDiffError -import com.sebastianneubauer.jsontree.diff.JsonTreeDiffState +import com.sebastianneubauer.jsontree.diff.JsonTreeDiffInfo import com.sebastianneubauer.jsontree.diff.defaultDarkDiffColors import com.sebastianneubauer.jsontree.diff.defaultLightDiffColors import com.sebastianneubauer.jsontree.search.rememberSearchState @@ -369,14 +366,16 @@ private fun JsonDiff( showInlineDiffs: Boolean, colors: TreeColors ) { - var state by remember(originalJson) { mutableStateOf(null) } - val currentState = state + var diffInfo by remember(originalJson, showInlineDiffs) { mutableStateOf(null) } + val currentDiffInfo = diffInfo Column { - val lineChanges = if(currentState is JsonTreeDiffState.Success) { - when(val changeInfo = currentState.info.changeInfo) { - is ChangeInfo.Identical -> "Identical" - is ChangeInfo.Changed -> "+${changeInfo.insertions} -${changeInfo.deletions}" + val lineChanges = if(currentDiffInfo != null) { + val changeInfo = currentDiffInfo.changeInfo + if(changeInfo.insertions == 0 && changeInfo.deletions == 0) { + "Identical" + } else { + "+${changeInfo.insertions} -${changeInfo.deletions}" } } else null @@ -384,20 +383,20 @@ private fun JsonDiff( Text( modifier = Modifier.weight(1F), text = "Original:", - color = if (colors == defaultLightColors) Color.Black else Color.White + color = Color.Black ) Row(modifier = Modifier.weight(1F)) { Text( modifier = Modifier.weight(1F), text = "Revised:", - color = if (colors == defaultLightColors) Color.Black else Color.White + color = Color.Black ) if(lineChanges != null) { Text( text = lineChanges, - color = if (colors == defaultLightColors) Color.Black else Color.White + color = Color.Black ) } } @@ -411,39 +410,34 @@ private fun JsonDiff( revisedJson = complexJsonRevised, showInlineDiffs = showInlineDiffs, colors = if(colors == defaultLightColors) defaultLightDiffColors else defaultDarkDiffColors, - onStateChanged = { state = it }, - ) - } - - when(currentState) { - is JsonTreeDiffState.Loading -> { - Box( - modifier = Modifier - .fillMaxSize() - .background( - if (colors == defaultLightColors) Color.White else Color.Black - ), - contentAlignment = Alignment.Center - ) { + onSuccess = { diffInfo = it.info }, + onLoading = { + Box( + modifier = Modifier + .fillMaxSize() + .background( + if (colors == defaultLightColors) Color.White else Color.Black + ), + contentAlignment = Alignment.Center + ) { + Text( + text = "Loading...", + color = if (colors == defaultLightColors) Color.Black else Color.White + ) + } + }, + onError = { errorState -> Text( - text = "Loading...", - color = if (colors == defaultLightColors) Color.Black else Color.White + modifier = Modifier + .fillMaxSize() + .background( + color = if (colors == defaultLightColors) Color.White else Color.Black + ), + text = errorState.error.throwable.message ?: "Unknown error", + color = if (colors == defaultLightColors) Color.Black else Color.White, ) } - } - is JsonTreeDiffState.Error -> { - Text( - modifier = Modifier - .fillMaxSize() - .background( - color = if (colors == defaultLightColors) Color.White else Color.Black - ), - text = currentState.error.throwable.message ?: "Unknown error", - color = if (colors == defaultLightColors) Color.Black else Color.White, - ) - } - is JsonTreeDiffState.Success, - null -> Unit + ) } } From b13941faa06f0e2871ae2053802d0d7d2f27245c Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sat, 4 Apr 2026 12:30:53 +0000 Subject: [PATCH 20/32] Adjust tests --- .../jsontree/diff/JsonTreeDifferTest.kt | 82 ++++++++++++++++--- 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt index 43f19a1..36f7e33 100644 --- a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt +++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferTest.kt @@ -50,6 +50,12 @@ public class JsonTreeDifferTest { assertEquals( expected = JsonTreeDifferState.Ready( + diffInfo = JsonTreeDiffInfo( + changeInfo = ChangeInfo( + insertions = 0, + deletions = 0 + ) + ), diffElements = listOf( Pair( JsonDiffElement.Equal( @@ -135,6 +141,12 @@ public class JsonTreeDifferTest { assertEquals( expected = JsonTreeDifferState.Ready( + diffInfo = JsonTreeDiffInfo( + changeInfo = ChangeInfo( + insertions = 1, + deletions = 1 + ) + ), diffElements = listOf( Pair( JsonDiffElement.Equal( @@ -190,7 +202,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.differState.value + actual = differ.state.value ) } @@ -235,6 +247,12 @@ public class JsonTreeDifferTest { assertEquals( expected = JsonTreeDifferState.Ready( + diffInfo = JsonTreeDiffInfo( + changeInfo = ChangeInfo( + insertions = 1, + deletions = 0 + ) + ), diffElements = listOf( Pair( JsonDiffElement.Equal( @@ -291,7 +309,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.differState.value + actual = differ.state.value ) } @@ -336,6 +354,12 @@ public class JsonTreeDifferTest { assertEquals( expected = JsonTreeDifferState.Ready( + diffInfo = JsonTreeDiffInfo( + changeInfo = ChangeInfo( + insertions = 0, + deletions = 1 + ) + ), diffElements = listOf( Pair( JsonDiffElement.Equal( @@ -392,7 +416,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.differState.value + actual = differ.state.value ) } @@ -428,6 +452,12 @@ public class JsonTreeDifferTest { assertEquals( expected = JsonTreeDifferState.Ready( + diffInfo = JsonTreeDiffInfo( + changeInfo = ChangeInfo( + insertions = 1, + deletions = 1 + ) + ), diffElements = listOf( Pair( JsonDiffElement.Equal( @@ -483,7 +513,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.differState.value + actual = differ.state.value ) } @@ -497,7 +527,7 @@ public class JsonTreeDifferTest { showInlineDiffs = false ) - val state = differ.differState.value + val state = differ.state.value assertTrue(state is JsonTreeDifferState.Error.OriginalJsonError) } @@ -511,7 +541,7 @@ public class JsonTreeDifferTest { showInlineDiffs = false ) - val state = differ.differState.value + val state = differ.state.value assertTrue(state is JsonTreeDifferState.Error.RevisedJsonError) } @@ -557,6 +587,12 @@ public class JsonTreeDifferTest { assertEquals( expected = JsonTreeDifferState.Ready( + diffInfo = JsonTreeDiffInfo( + changeInfo = ChangeInfo( + insertions = 0, + deletions = 0 + ) + ), diffElements = listOf( Pair( JsonDiffElement.Equal(jsonTreeElement = rootObject), @@ -608,7 +644,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.differState.value + actual = differ.state.value ) } @@ -684,6 +720,12 @@ public class JsonTreeDifferTest { assertEquals( expected = JsonTreeDifferState.Ready( + diffInfo = JsonTreeDiffInfo( + changeInfo = ChangeInfo( + insertions = 1, + deletions = 1 + ) + ), diffElements = listOf( Pair( JsonDiffElement.Equal(jsonTreeElement = originalRootObject), @@ -741,7 +783,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.differState.value + actual = differ.state.value ) } @@ -797,6 +839,12 @@ public class JsonTreeDifferTest { assertEquals( expected = JsonTreeDifferState.Ready( + diffInfo = JsonTreeDiffInfo( + changeInfo = ChangeInfo( + insertions = 0, + deletions = 0 + ) + ), diffElements = listOf( Pair( JsonDiffElement.Equal(jsonTreeElement = rootObject), @@ -870,7 +918,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.differState.value + actual = differ.state.value ) } @@ -958,6 +1006,12 @@ public class JsonTreeDifferTest { assertEquals( expected = JsonTreeDifferState.Ready( + diffInfo = JsonTreeDiffInfo( + changeInfo = ChangeInfo( + insertions = 1, + deletions = 0 + ) + ), diffElements = listOf( Pair( JsonDiffElement.Equal(jsonTreeElement = originalRootObject), @@ -1013,7 +1067,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.differState.value + actual = differ.state.value ) } @@ -1101,6 +1155,12 @@ public class JsonTreeDifferTest { assertEquals( expected = JsonTreeDifferState.Ready( + diffInfo = JsonTreeDiffInfo( + changeInfo = ChangeInfo( + insertions = 0, + deletions = 1 + ) + ), diffElements = listOf( Pair( JsonDiffElement.Equal(jsonTreeElement = originalRootObject), @@ -1156,7 +1216,7 @@ public class JsonTreeDifferTest { ) ) ), - actual = differ.differState.value + actual = differ.state.value ) } } \ No newline at end of file From de8256a4dc73c3d02d337e12abce5285709c4903 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sat, 4 Apr 2026 14:08:36 +0000 Subject: [PATCH 21/32] Improve performance --- .../jsontree/diff/JsonTreeDiffer.kt | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index d63c101..5bb01e1 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -20,10 +20,12 @@ import io.github.petertrr.diffutils.text.DiffRow import io.github.petertrr.diffutils.text.DiffRowGenerator import io.github.petertrr.diffutils.text.DiffTagGenerator import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.NonCancellable.start import kotlinx.coroutines.async import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlin.collections.orEmpty +import kotlin.time.Clock internal class JsonTreeDiffer( val defaultDispatcher: CoroutineDispatcher, @@ -61,6 +63,9 @@ internal class JsonTreeDiffer( } } + val originalDiffRowInput = async { originalJsonTreeList.fastMap { it.toRenderString() } } + val revisedDiffRowInput = async { revisedJsonTreeList.fastMap { it.toRenderString() } } + val originalJsonTreeMapDeferred = async { originalJsonTreeList.groupBy { it.toRenderString() } } val revisedJsonTreeMapDeferred = async { revisedJsonTreeList.groupBy { it.toRenderString() } } @@ -76,8 +81,8 @@ internal class JsonTreeDiffer( newTag = diffTagGenerator, oldTag = diffTagGenerator, ).generateDiffRows( - originalJsonTreeList.fastMap { it.toRenderString() }, - revisedJsonTreeList.fastMap { it.toRenderString() } + originalDiffRowInput.await(), + revisedDiffRowInput.await() ) val diffInfoDeferred = async { @@ -96,8 +101,8 @@ internal class JsonTreeDiffer( val originalJsonTreeMap = originalJsonTreeMapDeferred.await() val revisedJsonTreeMap = revisedJsonTreeMapDeferred.await() - val usedOriginalIds = mutableMapOf>() - val usedRevisedIds = mutableMapOf>() + val usedOriginalIds = mutableMapOf>() + val usedRevisedIds = mutableMapOf>() val diffElements = diffRows.fastMap { diffRow -> val (oldLineDiffIndices, newLineDiffIndices) = if(diffRow.tag == DiffRow.Tag.CHANGE && showInlineDiffs) { @@ -146,18 +151,18 @@ internal class JsonTreeDiffer( private fun getOriginalDiffElement( diffRow: DiffRow, - usedIds: MutableMap>, + usedIds: MutableMap>, originalJsonTreeMap: Map>, inlineDiffsIndices: List> ): JsonDiffElement { fun findJsonTreeElement(): JsonTreeElement { val jsonTreeElements = originalJsonTreeMap.getValue(diffRow.oldLine) - return jsonTreeElements.fastFirst { it.id !in usedIds[diffRow.oldLine].orEmpty() } + val usedIdsList = usedIds[diffRow.oldLine].orEmpty() + return jsonTreeElements.fastFirst { it.id !in usedIdsList } } fun addToUsedIds(id: String) { - val currentIds = usedIds[diffRow.oldLine] ?: mutableListOf() - usedIds[diffRow.oldLine] = currentIds.apply { add(id) } + usedIds.getOrPut(diffRow.oldLine) { mutableSetOf() }.add(id) } return when(diffRow.tag) { @@ -187,18 +192,18 @@ internal class JsonTreeDiffer( private fun getRevisedDiffElement( diffRow: DiffRow, - usedIds: MutableMap>, + usedIds: MutableMap>, revisedJsonTreeMap: Map>, inlineDiffsIndices: List> ): JsonDiffElement { fun findJsonTreeElement(): JsonTreeElement { val jsonTreeElements = revisedJsonTreeMap.getValue(diffRow.newLine) - return jsonTreeElements.fastFirst { it.id !in usedIds[diffRow.newLine].orEmpty() } + val usedIdsList = usedIds[diffRow.newLine].orEmpty() + return jsonTreeElements.fastFirst { it.id !in usedIdsList } } fun addToUsedIds(id: String) { - val currentIds = usedIds[diffRow.newLine] ?: mutableListOf() - usedIds[diffRow.newLine] = currentIds.apply { add(id) } + usedIds.getOrPut(diffRow.newLine) { mutableSetOf() }.add(id) } return when(diffRow.tag) { From fe54f3511d24fe2b66095f3df0992b2fe5d97820 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sat, 4 Apr 2026 14:32:57 +0000 Subject: [PATCH 22/32] More performance improvements --- .../jsontree/diff/JsonTreeDiffer.kt | 69 ++++++------------- 1 file changed, 22 insertions(+), 47 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index 5bb01e1..3475fac 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -2,7 +2,6 @@ package com.sebastianneubauer.jsontree.diff import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.util.fastFirst import androidx.compose.ui.util.fastMap import com.sebastianneubauer.jsontree.JsonTreeElement import com.sebastianneubauer.jsontree.JsonTreeElement.ParentType @@ -20,12 +19,9 @@ import io.github.petertrr.diffutils.text.DiffRow import io.github.petertrr.diffutils.text.DiffRowGenerator import io.github.petertrr.diffutils.text.DiffTagGenerator import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.NonCancellable.start import kotlinx.coroutines.async import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json -import kotlin.collections.orEmpty -import kotlin.time.Clock internal class JsonTreeDiffer( val defaultDispatcher: CoroutineDispatcher, @@ -66,8 +62,16 @@ internal class JsonTreeDiffer( val originalDiffRowInput = async { originalJsonTreeList.fastMap { it.toRenderString() } } val revisedDiffRowInput = async { revisedJsonTreeList.fastMap { it.toRenderString() } } - val originalJsonTreeMapDeferred = async { originalJsonTreeList.groupBy { it.toRenderString() } } - val revisedJsonTreeMapDeferred = async { revisedJsonTreeList.groupBy { it.toRenderString() } } + val originalJsonTreeMapDeferred = async { + originalJsonTreeList + .groupBy { it.toRenderString() } + .mapValues { it.value.toMutableList() } + } + val revisedJsonTreeMapDeferred = async { + revisedJsonTreeList + .groupBy { it.toRenderString() } + .mapValues { it.value.toMutableList() } + } val diffTagGenerator = object : DiffTagGenerator { override fun generateClose(tag: DiffRow.Tag): String = inlineDiffTagClosed @@ -101,9 +105,6 @@ internal class JsonTreeDiffer( val originalJsonTreeMap = originalJsonTreeMapDeferred.await() val revisedJsonTreeMap = revisedJsonTreeMapDeferred.await() - val usedOriginalIds = mutableMapOf>() - val usedRevisedIds = mutableMapOf>() - val diffElements = diffRows.fastMap { diffRow -> val (oldLineDiffIndices, newLineDiffIndices) = if(diffRow.tag == DiffRow.Tag.CHANGE && showInlineDiffs) { val oldLineIndices = diffRow.oldLine.findInlineDiffTagIndices() @@ -121,7 +122,6 @@ internal class JsonTreeDiffer( val originalDiffElementDeferred = async { getOriginalDiffElement( diffRow = strippedTagDiffRow, - usedIds = usedOriginalIds, originalJsonTreeMap = originalJsonTreeMap, inlineDiffsIndices = oldLineDiffIndices ) @@ -130,7 +130,6 @@ internal class JsonTreeDiffer( val revisedDiffElementDeferred = async { getRevisedDiffElement( diffRow = strippedTagDiffRow, - usedIds = usedRevisedIds, revisedJsonTreeMap = revisedJsonTreeMap, inlineDiffsIndices = newLineDiffIndices ) @@ -151,31 +150,21 @@ internal class JsonTreeDiffer( private fun getOriginalDiffElement( diffRow: DiffRow, - usedIds: MutableMap>, - originalJsonTreeMap: Map>, + originalJsonTreeMap: Map>, inlineDiffsIndices: List> ): JsonDiffElement { - fun findJsonTreeElement(): JsonTreeElement { + fun findAndRemoveJsonTreeElement(): JsonTreeElement { val jsonTreeElements = originalJsonTreeMap.getValue(diffRow.oldLine) - val usedIdsList = usedIds[diffRow.oldLine].orEmpty() - return jsonTreeElements.fastFirst { it.id !in usedIdsList } - } - - fun addToUsedIds(id: String) { - usedIds.getOrPut(diffRow.oldLine) { mutableSetOf() }.add(id) + return jsonTreeElements.removeAt(0) } return when(diffRow.tag) { DiffRow.Tag.EQUAL -> { - val jsonTreeElement = findJsonTreeElement() - addToUsedIds(id = jsonTreeElement.id) - JsonDiffElement.Equal(jsonTreeElement) + JsonDiffElement.Equal(findAndRemoveJsonTreeElement()) } DiffRow.Tag.CHANGE -> { - val jsonTreeElement = findJsonTreeElement() - addToUsedIds(id = jsonTreeElement.id) JsonDiffElement.Change( - jsonTreeElement = jsonTreeElement, + jsonTreeElement = findAndRemoveJsonTreeElement(), inlineDiffIndices = inlineDiffsIndices ) } @@ -183,47 +172,33 @@ internal class JsonTreeDiffer( JsonDiffElement.Insertion(null) } DiffRow.Tag.DELETE -> { - val jsonTreeElement = findJsonTreeElement() - addToUsedIds(id = jsonTreeElement.id) - JsonDiffElement.Deletion(jsonTreeElement) + JsonDiffElement.Deletion(findAndRemoveJsonTreeElement()) } } } private fun getRevisedDiffElement( diffRow: DiffRow, - usedIds: MutableMap>, - revisedJsonTreeMap: Map>, + revisedJsonTreeMap: Map>, inlineDiffsIndices: List> ): JsonDiffElement { - fun findJsonTreeElement(): JsonTreeElement { + fun findAndRemoveJsonTreeElement(): JsonTreeElement { val jsonTreeElements = revisedJsonTreeMap.getValue(diffRow.newLine) - val usedIdsList = usedIds[diffRow.newLine].orEmpty() - return jsonTreeElements.fastFirst { it.id !in usedIdsList } - } - - fun addToUsedIds(id: String) { - usedIds.getOrPut(diffRow.newLine) { mutableSetOf() }.add(id) + return jsonTreeElements.removeAt(0) } return when(diffRow.tag) { DiffRow.Tag.EQUAL -> { - val jsonTreeElement = findJsonTreeElement() - addToUsedIds(id = jsonTreeElement.id) - JsonDiffElement.Equal(jsonTreeElement) + JsonDiffElement.Equal(findAndRemoveJsonTreeElement()) } DiffRow.Tag.CHANGE -> { - val jsonTreeElement = findJsonTreeElement() - addToUsedIds(id = jsonTreeElement.id) JsonDiffElement.Change( - jsonTreeElement = jsonTreeElement, + jsonTreeElement = findAndRemoveJsonTreeElement(), inlineDiffIndices = inlineDiffsIndices ) } DiffRow.Tag.INSERT -> { - val jsonTreeElement = findJsonTreeElement() - addToUsedIds(id = jsonTreeElement.id) - JsonDiffElement.Insertion(jsonTreeElement) + JsonDiffElement.Insertion(findAndRemoveJsonTreeElement()) } DiffRow.Tag.DELETE -> JsonDiffElement.Deletion(null) } From a6fb745144a75dae3bfa610ab40cc3ceaae6f6bf Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sun, 5 Apr 2026 09:59:39 +0000 Subject: [PATCH 23/32] Add comments and improve tag replacement --- .../jsontree/diff/JsonTreeDiff.kt | 16 +++++++++++++++- .../jsontree/diff/JsonTreeDiffer.kt | 11 ++++++----- sample/src/commonMain/kotlin/App.kt | 1 + 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index 665c1c6..0975d09 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -30,6 +30,20 @@ import com.sebastianneubauer.jsontree.diff.JsonTreeDiffError.OriginalJsonError import com.sebastianneubauer.jsontree.diff.JsonTreeDiffError.RevisedJsonError import kotlinx.coroutines.Dispatchers +/** + * Renders a side-by-side diff of two JSON strings with syntax highlighting. + * + * @param originalJson The original JSON data as a string. + * @param revisedJson The revised JSON data as a string which is compared with [originalJson]. + * @param onLoading A Composable which is show while the diff is being calculated. + * @param onError A Composable which is shown if an error occurs. Receives an [Error] object with more information. + * @param onSuccess A callback which is called when the diff calculation succeeded. Receives an [Success] object with more information. + * @param modifier The Modifier which is applied on the side-by-side diff. Not applied on the [onLoading] and [onError] slots. + * @param showInlineDiffs If true, the diff shows partial changes within the text of a line. + * @param contentPadding The content padding which is applied on the LazyColumn of the side-by-side diff. + * @param colors The color palette the diff uses. [defaultLightDiffColors], [defaultDarkDiffColors] or a custom instance of [JsonTreeDiffColors]. + * @param textStyle The style which is used for all texts in the diff. + */ @Composable public fun JsonTreeDiff( originalJson: String, @@ -38,7 +52,7 @@ public fun JsonTreeDiff( onError: @Composable (Error) -> Unit, onSuccess: (Success) -> Unit= {}, modifier: Modifier = Modifier, - showInlineDiffs: Boolean = true, + showInlineDiffs: Boolean = false, contentPadding: PaddingValues = PaddingValues(0.dp), colors: JsonTreeDiffColors = defaultLightDiffColors, textStyle: TextStyle = LocalTextStyle.current diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index 3475fac..5003931 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -114,10 +114,12 @@ internal class JsonTreeDiffer( Pair(emptyList(), emptyList()) } - val strippedTagDiffRow = diffRow.copy( - oldLine = diffRow.oldLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, ""), - newLine = diffRow.newLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, "") - ) + val strippedTagDiffRow = if(diffRow.tag != DiffRow.Tag.EQUAL && showInlineDiffs) { + diffRow.copy( + oldLine = diffRow.oldLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, ""), + newLine = diffRow.newLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, "") + ) + } else diffRow val originalDiffElementDeferred = async { getOriginalDiffElement( @@ -233,7 +235,6 @@ internal class JsonTreeDiffer( val openTagIndex = indexOf(inlineDiffTagOpen, currentIndex) if (openTagIndex == -1) break - // Add non-bold text length to stripped index strippedIndex += (openTagIndex - currentIndex) val contentStart = openTagIndex + inlineDiffTagOpen.length diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt index 2ddc406..4100786 100644 --- a/sample/src/commonMain/kotlin/App.kt +++ b/sample/src/commonMain/kotlin/App.kt @@ -410,6 +410,7 @@ private fun JsonDiff( revisedJson = complexJsonRevised, showInlineDiffs = showInlineDiffs, colors = if(colors == defaultLightColors) defaultLightDiffColors else defaultDarkDiffColors, + contentPadding = PaddingValues(8.dp), onSuccess = { diffInfo = it.info }, onLoading = { Box( From 1ed96cb3fc698201f9492bea0c438bd5ca6df936 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Sun, 5 Apr 2026 15:52:00 +0000 Subject: [PATCH 24/32] Rename states --- .../jsontree/diff/JsonTreeDiff.kt | 18 +++++++++--------- .../jsontree/diff/JsonTreeDiffState.kt | 10 +++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index 0975d09..c2e4922 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -26,8 +26,8 @@ import androidx.compose.ui.unit.dp import com.sebastianneubauer.jsontree.CollapsableType import com.sebastianneubauer.jsontree.JsonTreeElement import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement -import com.sebastianneubauer.jsontree.diff.JsonTreeDiffError.OriginalJsonError -import com.sebastianneubauer.jsontree.diff.JsonTreeDiffError.RevisedJsonError +import com.sebastianneubauer.jsontree.diff.DiffError.OriginalJsonError +import com.sebastianneubauer.jsontree.diff.DiffError.RevisedJsonError import kotlinx.coroutines.Dispatchers /** @@ -36,8 +36,8 @@ import kotlinx.coroutines.Dispatchers * @param originalJson The original JSON data as a string. * @param revisedJson The revised JSON data as a string which is compared with [originalJson]. * @param onLoading A Composable which is show while the diff is being calculated. - * @param onError A Composable which is shown if an error occurs. Receives an [Error] object with more information. - * @param onSuccess A callback which is called when the diff calculation succeeded. Receives an [Success] object with more information. + * @param onError A Composable which is shown if an error occurs. Receives an [JsonTreeDiffError] object with more information. + * @param onSuccess A callback which is called when the diff calculation succeeded. Receives an [JsonTreeDiffSuccess] object with more information. * @param modifier The Modifier which is applied on the side-by-side diff. Not applied on the [onLoading] and [onError] slots. * @param showInlineDiffs If true, the diff shows partial changes within the text of a line. * @param contentPadding The content padding which is applied on the LazyColumn of the side-by-side diff. @@ -49,8 +49,8 @@ public fun JsonTreeDiff( originalJson: String, revisedJson: String, onLoading: @Composable () -> Unit, - onError: @Composable (Error) -> Unit, - onSuccess: (Success) -> Unit= {}, + onError: @Composable (JsonTreeDiffError) -> Unit, + onSuccess: (JsonTreeDiffSuccess) -> Unit= {}, modifier: Modifier = Modifier, showInlineDiffs: Boolean = false, contentPadding: PaddingValues = PaddingValues(0.dp), @@ -76,7 +76,7 @@ public fun JsonTreeDiff( when(val state = jsonTreeDiffer.state.value) { is JsonTreeDifferState.Loading -> onLoading() is JsonTreeDifferState.Ready -> { - onSuccess(Success(state.diffInfo)) + onSuccess(JsonTreeDiffSuccess(state.diffInfo)) Box(modifier = modifier) { SideBySideDiff( @@ -87,8 +87,8 @@ public fun JsonTreeDiff( ) } } - is JsonTreeDifferState.Error.OriginalJsonError -> onError(Error(OriginalJsonError(state.throwable))) - is JsonTreeDifferState.Error.RevisedJsonError -> onError(Error(RevisedJsonError(state.throwable))) + is JsonTreeDifferState.Error.OriginalJsonError -> onError(JsonTreeDiffError(OriginalJsonError(state.throwable))) + is JsonTreeDifferState.Error.RevisedJsonError -> onError(JsonTreeDiffError(RevisedJsonError(state.throwable))) } } diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt index 79f1aff..2ff3f80 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt @@ -3,7 +3,7 @@ package com.sebastianneubauer.jsontree.diff /** * The diff calculation succeeded. See [info] for more details on the diff. */ -public data class Success(val info: JsonTreeDiffInfo) +public data class JsonTreeDiffSuccess(val info: JsonTreeDiffInfo) /** * Infos about the calculated diff. @@ -26,17 +26,17 @@ public data class ChangeInfo( /** * The diff calculation failed with an error. See [error] for details. */ -public data class Error(val error: JsonTreeDiffError) +public data class JsonTreeDiffError(val error: DiffError) -public interface JsonTreeDiffError { +public sealed interface DiffError { public val throwable: Throwable /** * Describes an error during parsing of the original Json. */ - public class OriginalJsonError(override val throwable: Throwable): JsonTreeDiffError + public class OriginalJsonError(override val throwable: Throwable): DiffError /** * Describes an error during parsing of the revised Json. */ - public class RevisedJsonError(override val throwable: Throwable): JsonTreeDiffError + public class RevisedJsonError(override val throwable: Throwable): DiffError } \ No newline at end of file From 63b367aec71cb3854e1b84fe9a62a9832c8e5a58 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Mon, 6 Apr 2026 13:09:16 +0000 Subject: [PATCH 25/32] dump api --- jsontree/api/android/jsontree.api | 109 ++++++++++++++++++++++++++++++ jsontree/api/jvm/jsontree.api | 109 ++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+) diff --git a/jsontree/api/android/jsontree.api b/jsontree/api/android/jsontree.api index 0c53af1..6a167ce 100644 --- a/jsontree/api/android/jsontree.api +++ b/jsontree/api/android/jsontree.api @@ -46,6 +46,115 @@ public final class com/sebastianneubauer/jsontree/TreeState : java/lang/Enum { public static fun values ()[Lcom/sebastianneubauer/jsontree/TreeState; } +public final class com/sebastianneubauer/jsontree/diff/ChangeInfo { + public static final field $stable I + public fun (II)V + public final fun component1 ()I + public final fun component2 ()I + public final fun copy (II)Lcom/sebastianneubauer/jsontree/diff/ChangeInfo; + public static synthetic fun copy$default (Lcom/sebastianneubauer/jsontree/diff/ChangeInfo;IIILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/diff/ChangeInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getDeletions ()I + public final fun getInsertions ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/sebastianneubauer/jsontree/diff/DiffError { + public abstract fun getThrowable ()Ljava/lang/Throwable; +} + +public final class com/sebastianneubauer/jsontree/diff/DiffError$OriginalJsonError : com/sebastianneubauer/jsontree/diff/DiffError { + public static final field $stable I + public fun (Ljava/lang/Throwable;)V + public fun getThrowable ()Ljava/lang/Throwable; +} + +public final class com/sebastianneubauer/jsontree/diff/DiffError$RevisedJsonError : com/sebastianneubauer/jsontree/diff/DiffError { + public static final field $stable I + public fun (Ljava/lang/Throwable;)V + public fun getThrowable ()Ljava/lang/Throwable; +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors { + public static final field $stable I + public synthetic fun (JJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-0d7_KjU ()J + public final fun component10-0d7_KjU ()J + public final fun component11-0d7_KjU ()J + public final fun component12-0d7_KjU ()J + public final fun component2-0d7_KjU ()J + public final fun component3-0d7_KjU ()J + public final fun component4-0d7_KjU ()J + public final fun component5-0d7_KjU ()J + public final fun component6-0d7_KjU ()J + public final fun component7-0d7_KjU ()J + public final fun component8-0d7_KjU ()J + public final fun component9-0d7_KjU ()J + public final fun copy-2qZNXz8 (JJJJJJJJJJJJ)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors; + public static synthetic fun copy-2qZNXz8$default (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors;JJJJJJJJJJJJILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors; + public fun equals (Ljava/lang/Object;)Z + public final fun getBooleanValueColor-0d7_KjU ()J + public final fun getChangeBackgroundColor-0d7_KjU ()J + public final fun getDeletionBackgroundColor-0d7_KjU ()J + public final fun getDeletionHighlightColor-0d7_KjU ()J + public final fun getInsertionBackgroundColor-0d7_KjU ()J + public final fun getInsertionHighlightColor-0d7_KjU ()J + public final fun getKeyColor-0d7_KjU ()J + public final fun getNullValueColor-0d7_KjU ()J + public final fun getNumberValueColor-0d7_KjU ()J + public final fun getRegularBackgroundColor-0d7_KjU ()J + public final fun getStringValueColor-0d7_KjU ()J + public final fun getSymbolColor-0d7_KjU ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffColorsKt { + public static final fun getDefaultDarkDiffColors ()Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors; + public static final fun getDefaultLightDiffColors ()Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors; +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffError { + public static final field $stable I + public fun (Lcom/sebastianneubauer/jsontree/diff/DiffError;)V + public final fun component1 ()Lcom/sebastianneubauer/jsontree/diff/DiffError; + public final fun copy (Lcom/sebastianneubauer/jsontree/diff/DiffError;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffError; + public static synthetic fun copy$default (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffError;Lcom/sebastianneubauer/jsontree/diff/DiffError;ILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffError; + public fun equals (Ljava/lang/Object;)Z + public final fun getError ()Lcom/sebastianneubauer/jsontree/diff/DiffError; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo { + public static final field $stable I + public fun (Lcom/sebastianneubauer/jsontree/diff/ChangeInfo;)V + public final fun component1 ()Lcom/sebastianneubauer/jsontree/diff/ChangeInfo; + public final fun copy (Lcom/sebastianneubauer/jsontree/diff/ChangeInfo;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo; + public static synthetic fun copy$default (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo;Lcom/sebastianneubauer/jsontree/diff/ChangeInfo;ILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getChangeInfo ()Lcom/sebastianneubauer/jsontree/diff/ChangeInfo; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffKt { + public static final fun JsonTreeDiff (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;ZLandroidx/compose/foundation/layout/PaddingValues;Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffSuccess { + public static final field $stable I + public fun (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo;)V + public final fun component1 ()Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo; + public final fun copy (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffSuccess; + public static synthetic fun copy$default (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffSuccess;Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo;ILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffSuccess; + public fun equals (Ljava/lang/Object;)Z + public final fun getInfo ()Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/sebastianneubauer/jsontree/search/SearchState { public static final field $stable I public final fun getQuery ()Ljava/lang/String; diff --git a/jsontree/api/jvm/jsontree.api b/jsontree/api/jvm/jsontree.api index 0c53af1..6a167ce 100644 --- a/jsontree/api/jvm/jsontree.api +++ b/jsontree/api/jvm/jsontree.api @@ -46,6 +46,115 @@ public final class com/sebastianneubauer/jsontree/TreeState : java/lang/Enum { public static fun values ()[Lcom/sebastianneubauer/jsontree/TreeState; } +public final class com/sebastianneubauer/jsontree/diff/ChangeInfo { + public static final field $stable I + public fun (II)V + public final fun component1 ()I + public final fun component2 ()I + public final fun copy (II)Lcom/sebastianneubauer/jsontree/diff/ChangeInfo; + public static synthetic fun copy$default (Lcom/sebastianneubauer/jsontree/diff/ChangeInfo;IIILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/diff/ChangeInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getDeletions ()I + public final fun getInsertions ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/sebastianneubauer/jsontree/diff/DiffError { + public abstract fun getThrowable ()Ljava/lang/Throwable; +} + +public final class com/sebastianneubauer/jsontree/diff/DiffError$OriginalJsonError : com/sebastianneubauer/jsontree/diff/DiffError { + public static final field $stable I + public fun (Ljava/lang/Throwable;)V + public fun getThrowable ()Ljava/lang/Throwable; +} + +public final class com/sebastianneubauer/jsontree/diff/DiffError$RevisedJsonError : com/sebastianneubauer/jsontree/diff/DiffError { + public static final field $stable I + public fun (Ljava/lang/Throwable;)V + public fun getThrowable ()Ljava/lang/Throwable; +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffColors { + public static final field $stable I + public synthetic fun (JJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-0d7_KjU ()J + public final fun component10-0d7_KjU ()J + public final fun component11-0d7_KjU ()J + public final fun component12-0d7_KjU ()J + public final fun component2-0d7_KjU ()J + public final fun component3-0d7_KjU ()J + public final fun component4-0d7_KjU ()J + public final fun component5-0d7_KjU ()J + public final fun component6-0d7_KjU ()J + public final fun component7-0d7_KjU ()J + public final fun component8-0d7_KjU ()J + public final fun component9-0d7_KjU ()J + public final fun copy-2qZNXz8 (JJJJJJJJJJJJ)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors; + public static synthetic fun copy-2qZNXz8$default (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors;JJJJJJJJJJJJILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors; + public fun equals (Ljava/lang/Object;)Z + public final fun getBooleanValueColor-0d7_KjU ()J + public final fun getChangeBackgroundColor-0d7_KjU ()J + public final fun getDeletionBackgroundColor-0d7_KjU ()J + public final fun getDeletionHighlightColor-0d7_KjU ()J + public final fun getInsertionBackgroundColor-0d7_KjU ()J + public final fun getInsertionHighlightColor-0d7_KjU ()J + public final fun getKeyColor-0d7_KjU ()J + public final fun getNullValueColor-0d7_KjU ()J + public final fun getNumberValueColor-0d7_KjU ()J + public final fun getRegularBackgroundColor-0d7_KjU ()J + public final fun getStringValueColor-0d7_KjU ()J + public final fun getSymbolColor-0d7_KjU ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffColorsKt { + public static final fun getDefaultDarkDiffColors ()Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors; + public static final fun getDefaultLightDiffColors ()Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors; +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffError { + public static final field $stable I + public fun (Lcom/sebastianneubauer/jsontree/diff/DiffError;)V + public final fun component1 ()Lcom/sebastianneubauer/jsontree/diff/DiffError; + public final fun copy (Lcom/sebastianneubauer/jsontree/diff/DiffError;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffError; + public static synthetic fun copy$default (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffError;Lcom/sebastianneubauer/jsontree/diff/DiffError;ILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffError; + public fun equals (Ljava/lang/Object;)Z + public final fun getError ()Lcom/sebastianneubauer/jsontree/diff/DiffError; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo { + public static final field $stable I + public fun (Lcom/sebastianneubauer/jsontree/diff/ChangeInfo;)V + public final fun component1 ()Lcom/sebastianneubauer/jsontree/diff/ChangeInfo; + public final fun copy (Lcom/sebastianneubauer/jsontree/diff/ChangeInfo;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo; + public static synthetic fun copy$default (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo;Lcom/sebastianneubauer/jsontree/diff/ChangeInfo;ILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo; + public fun equals (Ljava/lang/Object;)Z + public final fun getChangeInfo ()Lcom/sebastianneubauer/jsontree/diff/ChangeInfo; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffKt { + public static final fun JsonTreeDiff (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;ZLandroidx/compose/foundation/layout/PaddingValues;Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffColors;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffSuccess { + public static final field $stable I + public fun (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo;)V + public final fun component1 ()Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo; + public final fun copy (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffSuccess; + public static synthetic fun copy$default (Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffSuccess;Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo;ILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffSuccess; + public fun equals (Ljava/lang/Object;)Z + public final fun getInfo ()Lcom/sebastianneubauer/jsontree/diff/JsonTreeDiffInfo; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/sebastianneubauer/jsontree/search/SearchState { public static final field $stable I public final fun getQuery ()Ljava/lang/String; From fd13f5167ed1936454a62a6affd31d43e6f13a24 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Mon, 6 Apr 2026 13:14:44 +0000 Subject: [PATCH 26/32] Run detekt --- .../jsontree/JsonTreeParser.kt | 2 +- .../jsontree/diff/AnnotatedDiffText.kt | 3 +- .../jsontree/diff/JsonTreeDiff.kt | 34 +++++++++++-------- .../jsontree/diff/JsonTreeDiffState.kt | 7 ++-- .../jsontree/diff/JsonTreeDiffer.kt | 23 +++++++------ .../jsontree/diff/JsonTreeDifferState.kt | 21 ++++++------ .../jsontree/util/IdGenerator.kt | 2 +- .../util/JsonTreeElementExtensions.kt | 18 +++++----- 8 files changed, 57 insertions(+), 53 deletions(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt index be2c2d6..d515a95 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt @@ -11,8 +11,8 @@ import com.sebastianneubauer.jsontree.JsonTreeParserState.Loading import com.sebastianneubauer.jsontree.JsonTreeParserState.Parsing.Error import com.sebastianneubauer.jsontree.JsonTreeParserState.Parsing.Parsed import com.sebastianneubauer.jsontree.JsonTreeParserState.Ready -import com.sebastianneubauer.jsontree.util.IdGenerator import com.sebastianneubauer.jsontree.util.Expansion +import com.sebastianneubauer.jsontree.util.IdGenerator import com.sebastianneubauer.jsontree.util.collapse import com.sebastianneubauer.jsontree.util.expand import com.sebastianneubauer.jsontree.util.toJsonTreeElement diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt index 18d1bd8..37c1576 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.text.withStyle import com.sebastianneubauer.jsontree.CollapsableType import com.sebastianneubauer.jsontree.JsonTreeElement import com.sebastianneubauer.jsontree.JsonTreeElement.ParentType -import com.sebastianneubauer.jsontree.TreeColors import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.doubleOrNull @@ -129,4 +128,4 @@ internal fun rememberEndBracketDiffText( } } } -} \ No newline at end of file +} diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index c2e4922..6dedafa 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -25,9 +25,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.sebastianneubauer.jsontree.CollapsableType import com.sebastianneubauer.jsontree.JsonTreeElement -import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement import com.sebastianneubauer.jsontree.diff.DiffError.OriginalJsonError import com.sebastianneubauer.jsontree.diff.DiffError.RevisedJsonError +import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement import kotlinx.coroutines.Dispatchers /** @@ -50,7 +50,7 @@ public fun JsonTreeDiff( revisedJson: String, onLoading: @Composable () -> Unit, onError: @Composable (JsonTreeDiffError) -> Unit, - onSuccess: (JsonTreeDiffSuccess) -> Unit= {}, + onSuccess: (JsonTreeDiffSuccess) -> Unit = {}, modifier: Modifier = Modifier, showInlineDiffs: Boolean = false, contentPadding: PaddingValues = PaddingValues(0.dp), @@ -73,7 +73,7 @@ public fun JsonTreeDiff( ) } - when(val state = jsonTreeDiffer.state.value) { + when (val state = jsonTreeDiffer.state.value) { is JsonTreeDifferState.Loading -> onLoading() is JsonTreeDifferState.Ready -> { onSuccess(JsonTreeDiffSuccess(state.diffInfo)) @@ -107,7 +107,7 @@ private fun SideBySideDiff( items = state.diffElements, key = { index, _ -> index } ) { index, (originalDiffElement, revisedDiffElement) -> - val originalJsonTreeElement = when(originalDiffElement) { + val originalJsonTreeElement = when (originalDiffElement) { is JsonDiffElement.Change -> originalDiffElement.jsonTreeElement is JsonDiffElement.Deletion -> originalDiffElement.jsonTreeElement!! is JsonDiffElement.Equal -> originalDiffElement.jsonTreeElement @@ -115,14 +115,16 @@ private fun SideBySideDiff( } val originalText = rememberText( jsonTreeElement = originalJsonTreeElement, - diffIndices = if(originalDiffElement is JsonDiffElement.Change) { + diffIndices = if (originalDiffElement is JsonDiffElement.Change) { originalDiffElement.inlineDiffIndices - } else null, + } else { + null + }, colors = colors, highlightColor = colors.deletionHighlightColor, ) - val revisedJsonTreeElement = when(revisedDiffElement) { + val revisedJsonTreeElement = when (revisedDiffElement) { is JsonDiffElement.Change -> revisedDiffElement.jsonTreeElement is JsonDiffElement.Deletion -> null is JsonDiffElement.Equal -> revisedDiffElement.jsonTreeElement @@ -130,9 +132,11 @@ private fun SideBySideDiff( } val revisedText = rememberText( jsonTreeElement = revisedJsonTreeElement, - diffIndices = if(revisedDiffElement is JsonDiffElement.Change) { + diffIndices = if (revisedDiffElement is JsonDiffElement.Change) { revisedDiffElement.inlineDiffIndices - } else null, + } else { + null + }, colors = colors, highlightColor = colors.insertionHighlightColor ) @@ -146,14 +150,14 @@ private fun SideBySideDiff( modifier = Modifier .weight(1F) .fillMaxHeight(), - backgroundColor = when(originalDiffElement) { + backgroundColor = when (originalDiffElement) { is JsonDiffElement.Change -> colors.deletionBackgroundColor is JsonDiffElement.Deletion -> colors.deletionBackgroundColor is JsonDiffElement.Equal -> colors.regularBackgroundColor is JsonDiffElement.Insertion -> colors.changeBackgroundColor }, backgroundFillColor = colors.changeBackgroundColor, - indent = if(originalJsonTreeElement != null && index > 0) { + indent = if (originalJsonTreeElement != null && index > 0) { 20.dp * originalJsonTreeElement.level } else { 0.dp @@ -166,14 +170,14 @@ private fun SideBySideDiff( modifier = Modifier .weight(1F) .fillMaxHeight(), - backgroundColor = when(revisedDiffElement) { + backgroundColor = when (revisedDiffElement) { is JsonDiffElement.Change -> colors.insertionBackgroundColor is JsonDiffElement.Deletion -> colors.changeBackgroundColor is JsonDiffElement.Equal -> colors.regularBackgroundColor is JsonDiffElement.Insertion -> colors.insertionBackgroundColor }, backgroundFillColor = colors.changeBackgroundColor, - indent = if(revisedJsonTreeElement != null && index > 0) { + indent = if (revisedJsonTreeElement != null && index > 0) { 20.dp * revisedJsonTreeElement.level } else { 0.dp @@ -223,7 +227,7 @@ private fun rememberText( colors: JsonTreeDiffColors, highlightColor: Color, ): AnnotatedString { - return when(jsonTreeElement) { + return when (jsonTreeElement) { is JsonTreeElement.Collapsable.Array -> rememberCollapsableDiffText( type = CollapsableType.ARRAY, key = jsonTreeElement.key, @@ -258,4 +262,4 @@ private fun rememberText( ) null -> AnnotatedString("") } -} \ No newline at end of file +} diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt index 2ff3f80..a06fb7b 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffState.kt @@ -30,13 +30,14 @@ public data class JsonTreeDiffError(val error: DiffError) public sealed interface DiffError { public val throwable: Throwable + /** * Describes an error during parsing of the original Json. */ - public class OriginalJsonError(override val throwable: Throwable): DiffError + public class OriginalJsonError(override val throwable: Throwable) : DiffError /** * Describes an error during parsing of the revised Json. */ - public class RevisedJsonError(override val throwable: Throwable): DiffError -} \ No newline at end of file + public class RevisedJsonError(override val throwable: Throwable) : DiffError +} diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index 5003931..b15ab8d 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -6,10 +6,10 @@ import androidx.compose.ui.util.fastMap import com.sebastianneubauer.jsontree.JsonTreeElement import com.sebastianneubauer.jsontree.JsonTreeElement.ParentType import com.sebastianneubauer.jsontree.TreeState +import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.Error import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.JsonDiffElement import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.Loading import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.Ready -import com.sebastianneubauer.jsontree.diff.JsonTreeDifferState.Error import com.sebastianneubauer.jsontree.util.IdGenerator import com.sebastianneubauer.jsontree.util.toJsonTreeElement import com.sebastianneubauer.jsontree.util.toList @@ -73,7 +73,7 @@ internal class JsonTreeDiffer( .mapValues { it.value.toMutableList() } } - val diffTagGenerator = object : DiffTagGenerator { + val diffTagGenerator = object : DiffTagGenerator { override fun generateClose(tag: DiffRow.Tag): String = inlineDiffTagClosed override fun generateOpen(tag: DiffRow.Tag): String = inlineDiffTagOpen } @@ -106,7 +106,7 @@ internal class JsonTreeDiffer( val revisedJsonTreeMap = revisedJsonTreeMapDeferred.await() val diffElements = diffRows.fastMap { diffRow -> - val (oldLineDiffIndices, newLineDiffIndices) = if(diffRow.tag == DiffRow.Tag.CHANGE && showInlineDiffs) { + val (oldLineDiffIndices, newLineDiffIndices) = if (diffRow.tag == DiffRow.Tag.CHANGE && showInlineDiffs) { val oldLineIndices = diffRow.oldLine.findInlineDiffTagIndices() val newLineIndices = diffRow.newLine.findInlineDiffTagIndices() Pair(oldLineIndices, newLineIndices) @@ -114,12 +114,14 @@ internal class JsonTreeDiffer( Pair(emptyList(), emptyList()) } - val strippedTagDiffRow = if(diffRow.tag != DiffRow.Tag.EQUAL && showInlineDiffs) { + val strippedTagDiffRow = if (diffRow.tag != DiffRow.Tag.EQUAL && showInlineDiffs) { diffRow.copy( oldLine = diffRow.oldLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, ""), newLine = diffRow.newLine.replace(inlineDiffTagOpen, "").replace(inlineDiffTagClosed, "") ) - } else diffRow + } else { + diffRow + } val originalDiffElementDeferred = async { getOriginalDiffElement( @@ -160,7 +162,7 @@ internal class JsonTreeDiffer( return jsonTreeElements.removeAt(0) } - return when(diffRow.tag) { + return when (diffRow.tag) { DiffRow.Tag.EQUAL -> { JsonDiffElement.Equal(findAndRemoveJsonTreeElement()) } @@ -182,14 +184,14 @@ internal class JsonTreeDiffer( private fun getRevisedDiffElement( diffRow: DiffRow, revisedJsonTreeMap: Map>, - inlineDiffsIndices: List> + inlineDiffsIndices: List> ): JsonDiffElement { fun findAndRemoveJsonTreeElement(): JsonTreeElement { val jsonTreeElements = revisedJsonTreeMap.getValue(diffRow.newLine) return jsonTreeElements.removeAt(0) } - return when(diffRow.tag) { + return when (diffRow.tag) { DiffRow.Tag.EQUAL -> { JsonDiffElement.Equal(findAndRemoveJsonTreeElement()) } @@ -253,11 +255,10 @@ internal class JsonTreeDiffer( } private sealed interface ParsingResult { - data class Success(val list: List): ParsingResult - data class Failure(val throwable: Throwable): ParsingResult + data class Success(val list: List) : ParsingResult + data class Failure(val throwable: Throwable) : ParsingResult } private val inlineDiffTagOpen = "JSON_TREE_DIFF_START_TAG" private val inlineDiffTagClosed = "JSON_TREE_DIFF_END_TAG" } - diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt index 5918a9c..8dca2b6 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDifferState.kt @@ -2,41 +2,40 @@ package com.sebastianneubauer.jsontree.diff import androidx.compose.runtime.Immutable import com.sebastianneubauer.jsontree.JsonTreeElement -import kotlinx.serialization.json.Json @Immutable internal sealed interface JsonTreeDifferState { - data object Loading: JsonTreeDifferState + data object Loading : JsonTreeDifferState data class Ready( val diffElements: List>, val diffInfo: JsonTreeDiffInfo - ): JsonTreeDifferState + ) : JsonTreeDifferState @Immutable - sealed interface Error: JsonTreeDifferState { - data class OriginalJsonError(val throwable: Throwable): Error - data class RevisedJsonError(val throwable: Throwable): Error + sealed interface Error : JsonTreeDifferState { + data class OriginalJsonError(val throwable: Throwable) : Error + data class RevisedJsonError(val throwable: Throwable) : Error } @Immutable - sealed interface JsonDiffElement{ + sealed interface JsonDiffElement { data class Change( val jsonTreeElement: JsonTreeElement, val inlineDiffIndices: List> - ): JsonDiffElement + ) : JsonDiffElement data class Insertion( val jsonTreeElement: JsonTreeElement?, - ): JsonDiffElement + ) : JsonDiffElement data class Deletion( val jsonTreeElement: JsonTreeElement?, - ): JsonDiffElement + ) : JsonDiffElement data class Equal( val jsonTreeElement: JsonTreeElement, - ): JsonDiffElement + ) : JsonDiffElement } } diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/IdGenerator.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/IdGenerator.kt index 121a3d7..0ab2463 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/IdGenerator.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/IdGenerator.kt @@ -7,4 +7,4 @@ internal class IdGenerator { fun incrementAndGet(): Long { return atomicLong.incrementAndGet() } -} \ No newline at end of file +} diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/JsonTreeElementExtensions.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/JsonTreeElementExtensions.kt index 388e396..b6f319d 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/JsonTreeElementExtensions.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/util/JsonTreeElementExtensions.kt @@ -4,8 +4,8 @@ import com.sebastianneubauer.jsontree.JsonTreeElement import com.sebastianneubauer.jsontree.JsonTreeElement.Collapsable.Array import com.sebastianneubauer.jsontree.JsonTreeElement.Collapsable.Object import com.sebastianneubauer.jsontree.JsonTreeElement.EndBracket -import com.sebastianneubauer.jsontree.JsonTreeElement.Primitive import com.sebastianneubauer.jsontree.JsonTreeElement.ParentType +import com.sebastianneubauer.jsontree.JsonTreeElement.Primitive import com.sebastianneubauer.jsontree.TreeState import com.sebastianneubauer.jsontree.endBracket import kotlinx.serialization.json.JsonArray @@ -267,25 +267,25 @@ internal fun JsonElement.toJsonTreeElement( * Converts a JsonTreeElement to its string representation. */ internal fun JsonTreeElement.toRenderString(): String { - return when(this) { - is Object -> if(key != null && parentType != ParentType.ARRAY) { + return when (this) { + is Object -> if (key != null && parentType != ParentType.ARRAY) { "\"$key\": {" } else { "{" } - is Array -> if(key != null && parentType != ParentType.ARRAY) { + is Array -> if (key != null && parentType != ParentType.ARRAY) { "\"$key\": [" } else { "[" } - is Primitive -> if(key != null && parentType != ParentType.ARRAY) { - "\"$key\": $value" + if(isLastItem) "" else "," + is Primitive -> if (key != null && parentType != ParentType.ARRAY) { + "\"$key\": $value" + if (isLastItem) "" else "," } else { - "$value" + if(isLastItem) "" else "," + "$value" + if (isLastItem) "" else "," } - is EndBracket -> when(type) { + is EndBracket -> when (type) { EndBracket.Type.ARRAY -> if (!isLastItem) "]," else "]" EndBracket.Type.OBJECT -> if (!isLastItem) "}," else "}" } } -} \ No newline at end of file +} From e0f6d812a7b73a6f2c763634ccaa445a4b322137 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Mon, 6 Apr 2026 13:40:11 +0000 Subject: [PATCH 27/32] Adjust detekt and CI --- .github/workflows/check-pr.yaml | 3 ++- detekt/config.yml | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-pr.yaml b/.github/workflows/check-pr.yaml index 4aa56ff..9da8064 100644 --- a/.github/workflows/check-pr.yaml +++ b/.github/workflows/check-pr.yaml @@ -81,7 +81,8 @@ jobs: ./gradlew :jsontree:jvmTest --tests com.sebastianneubauer.jsontree.JsonTreeParserTest --stacktrace ./gradlew :jsontree:jvmTest --tests com.sebastianneubauer.jsontree.JsonTreeSearchTest --stacktrace ./gradlew :jsontree:jvmTest --tests com.sebastianneubauer.jsontree.SearchStateTest --stacktrace - ./gradlew :jsontree:jvmTest --tests com.sebastianneubauer.jsontree.ExtensionsTest --stacktrace + ./gradlew :jsontree:jvmTest --tests com.sebastianneubauer.jsontree.JsonTreeElementExtensionsTest --stacktrace + ./gradlew :jsontree:jvmTest --tests com.sebastianneubauer.jsontree.diff.JsonTreeDifferTest --stacktrace apiCheck: runs-on: macos-latest diff --git a/detekt/config.yml b/detekt/config.yml index d9d5938..7d33a2b 100644 --- a/detekt/config.yml +++ b/detekt/config.yml @@ -26,5 +26,9 @@ style: active: false UnusedPrivateMember: ignoreAnnotated: ['Preview'] + LoopWithTooManyJumpStatements: + active: true + MaxLineLength: + active: false From 8769a70b0c9690b744525c8c0501d26f506bc370 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Mon, 6 Apr 2026 13:52:33 +0000 Subject: [PATCH 28/32] Adjust detekt --- detekt/config.yml | 6 ++++-- .../sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt | 1 - .../com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt | 2 -- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/detekt/config.yml b/detekt/config.yml index 7d33a2b..37e5185 100644 --- a/detekt/config.yml +++ b/detekt/config.yml @@ -17,7 +17,7 @@ complexity: LongParameterList: functionThreshold: 20 CyclomaticComplexMethod: - threshold: 20 + threshold: 25 LargeClass: threshold: 1000 @@ -27,8 +27,10 @@ style: UnusedPrivateMember: ignoreAnnotated: ['Preview'] LoopWithTooManyJumpStatements: - active: true + active: false MaxLineLength: active: false + MaximumLineLength: + active: false diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt index 37c1576..ffc108d 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/AnnotatedDiffText.kt @@ -23,7 +23,6 @@ internal fun rememberCollapsableDiffText( key: String?, colors: JsonTreeDiffColors, highlightColor: Color, - isLastItem: Boolean, parentType: ParentType, diffIndices: List>?, ): AnnotatedString { diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt index 6dedafa..426a189 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiff.kt @@ -231,7 +231,6 @@ private fun rememberText( is JsonTreeElement.Collapsable.Array -> rememberCollapsableDiffText( type = CollapsableType.ARRAY, key = jsonTreeElement.key, - isLastItem = jsonTreeElement.isLastItem, parentType = jsonTreeElement.parentType, colors = colors, highlightColor = highlightColor, @@ -240,7 +239,6 @@ private fun rememberText( is JsonTreeElement.Collapsable.Object -> rememberCollapsableDiffText( type = CollapsableType.OBJECT, key = jsonTreeElement.key, - isLastItem = jsonTreeElement.isLastItem, parentType = jsonTreeElement.parentType, colors = colors, highlightColor = highlightColor, From 3b66a07c6fdced6866f46af88ab6e49e27a50a38 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Mon, 6 Apr 2026 14:02:01 +0000 Subject: [PATCH 29/32] Adjust detekt --- detekt/config.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/detekt/config.yml b/detekt/config.yml index 37e5185..35bb4d0 100644 --- a/detekt/config.yml +++ b/detekt/config.yml @@ -30,7 +30,5 @@ style: active: false MaxLineLength: active: false - MaximumLineLength: - active: false From 8d4dcbd851333c8cfa4d939910af70313f443369 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Mon, 6 Apr 2026 14:14:42 +0000 Subject: [PATCH 30/32] fix detekt --- detekt/config.yml | 2 ++ .../com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/detekt/config.yml b/detekt/config.yml index 35bb4d0..37e5185 100644 --- a/detekt/config.yml +++ b/detekt/config.yml @@ -30,5 +30,7 @@ style: active: false MaxLineLength: active: false + MaximumLineLength: + active: false diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index b15ab8d..756d2c3 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -55,7 +55,8 @@ internal class JsonTreeDiffer( return@withContext } else -> { - (originalJsonTreeListResult as ParsingResult.Success).list to (revisedJsonTreeListResult as ParsingResult.Success).list + (originalJsonTreeListResult as ParsingResult.Success).list to + (revisedJsonTreeListResult as ParsingResult.Success).list } } From 719c576c134271188438a429c78a7c8ce8712990 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Mon, 6 Apr 2026 14:26:01 +0000 Subject: [PATCH 31/32] fix detekt --- detekt/config.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/detekt/config.yml b/detekt/config.yml index 37e5185..35bb4d0 100644 --- a/detekt/config.yml +++ b/detekt/config.yml @@ -30,7 +30,5 @@ style: active: false MaxLineLength: active: false - MaximumLineLength: - active: false From cef15c9a32040d0423d00f2fd5644a728e46a16d Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Mon, 6 Apr 2026 14:33:30 +0000 Subject: [PATCH 32/32] fix indentation --- .../com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt index 756d2c3..75a738c 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/diff/JsonTreeDiffer.kt @@ -56,7 +56,7 @@ internal class JsonTreeDiffer( } else -> { (originalJsonTreeListResult as ParsingResult.Success).list to - (revisedJsonTreeListResult as ParsingResult.Success).list + (revisedJsonTreeListResult as ParsingResult.Success).list } }