From 07cff6c7edb8170480cbbf8e4aa930af9e3ae07c Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Fri, 4 Sep 2026 14:30:10 +0200 Subject: [PATCH 1/2] Add case-sensitive option to search state --- .../sebastianneubauer/jsontree/JsonTree.kt | 4 +- .../jsontree/search/JsonTreeSearch.kt | 8 +- .../jsontree/search/SearchState.kt | 11 +++ .../jsontree/JsonTreeSearchTest.kt | 95 ++++++++++++++++++- .../jsontree/SearchStateTest.kt | 12 +++ .../composeResources/drawable/match_case.xml | 9 ++ .../sebastianneubauer/jsontreesample/App.kt | 12 +++ 7 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 shared/src/commonMain/composeResources/drawable/match_case.xml diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt index 463c9eb..76e1d12 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt @@ -126,13 +126,15 @@ public fun JsonTree( ) val searchQuery = searchState.state.query - LaunchedEffect(searchQuery) { + val caseSensitive = searchState.state.caseSensitive + LaunchedEffect(searchQuery, caseSensitive) { if (searchQuery == null) { searchState.reset() } else { val expandedList = jsonParser.expandAllItems() val searchResult = jsonSearch.search( searchQuery = searchQuery, + caseSensitive = caseSensitive, jsonTreeList = expandedList, ) searchState.state = searchResult diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/search/JsonTreeSearch.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/search/JsonTreeSearch.kt index 9548081..68a0a25 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/search/JsonTreeSearch.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/search/JsonTreeSearch.kt @@ -37,10 +37,15 @@ internal class JsonTreeSearch( ) { suspend fun search( searchQuery: String, + caseSensitive: Boolean, jsonTreeList: List, ): SearchResult = withContext(defaultDispatcher) { // create Regex here for better performance - val searchRegex = Regex("(?i)${Regex.escape(searchQuery)}") + val searchRegex = if(caseSensitive) { + Regex("(?)${Regex.escape(searchQuery)}") + } else { + Regex("(?i)${Regex.escape(searchQuery)}") + } val searchOccurrences = buildMap { jsonTreeList.forEachIndexed { index, jsonTreeElement -> @@ -61,6 +66,7 @@ internal class JsonTreeSearch( SearchResult( query = searchQuery, + caseSensitive = caseSensitive, occurrences = searchOccurrences, totalResults = searchOccurrences.values.sumOf { it.ranges.size }, selectedResultIndex = if (searchOccurrences.isNotEmpty()) 0 else null, diff --git a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/search/SearchState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/search/SearchState.kt index a51ccc7..19e5977 100644 --- a/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/search/SearchState.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/search/SearchState.kt @@ -34,6 +34,7 @@ public class SearchState internal constructor( internal var state: SearchResult by mutableStateOf( SearchResult( query = null, + caseSensitive = false, occurrences = emptyMap(), selectedOccurrence = null, totalResults = 0, @@ -48,6 +49,14 @@ public class SearchState internal constructor( set(value) { state = state.copy(query = value) } get() = state.query + /** + * Set to true if the result should be case-sensitive. + * Returns true if the result is case-sensitive. + */ + public var caseSensitive: Boolean + set(value) { state = state.copy(caseSensitive = value) } + get() = state.caseSensitive + /** * The total amount of results found for the [query]. */ @@ -172,6 +181,7 @@ public class SearchState internal constructor( internal fun reset() { state = SearchResult( query = null, + caseSensitive = false, occurrences = emptyMap(), selectedOccurrence = null, totalResults = 0, @@ -182,6 +192,7 @@ public class SearchState internal constructor( @Immutable internal data class SearchResult( val query: String?, + val caseSensitive: Boolean, val occurrences: Map, val selectedOccurrence: SelectedSearchOccurrence?, val totalResults: Int, diff --git a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeSearchTest.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeSearchTest.kt index a0305a3..788698b 100644 --- a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeSearchTest.kt +++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeSearchTest.kt @@ -1,5 +1,6 @@ package com.sebastianneubauer.jsontree +import androidx.compose.runtime.key import com.sebastianneubauer.jsontree.JsonTreeElement.Collapsable.Array import com.sebastianneubauer.jsontree.JsonTreeElement.Collapsable.Object import com.sebastianneubauer.jsontree.JsonTreeElement.Primitive.Type @@ -24,7 +25,11 @@ public class JsonTreeSearchTest { @Test public fun query_with_one_occurrence_in_key_should_return_result_with_one_occurrence_in_key(): TestResult = runTest { - val result = underTest.search(searchQuery = "array1", jsonTreeList = TestData.expandedList) + val result = underTest.search( + searchQuery = "array1", + caseSensitive = false, + jsonTreeList = TestData.expandedList + ) val range = SearchOccurrence.Range.Key(range = IntRange(0, 5)) val occurrence = SearchOccurrence( @@ -35,6 +40,7 @@ public class JsonTreeSearchTest { actual = result, expected = SearchResult( query = "array1", + caseSensitive = false, occurrences = mapOf(1 to occurrence), selectedOccurrence = SelectedSearchOccurrence( occurrence = occurrence, @@ -48,7 +54,11 @@ public class JsonTreeSearchTest { @Test public fun query_with_multiple_occurrences_in_key_should_return_result_with_multiple_occurrence_in_key(): TestResult = runTest { - val result = underTest.search(searchQuery = "i", jsonTreeList = TestData.expandedList) + val result = underTest.search( + searchQuery = "i", + caseSensitive = false, + jsonTreeList = TestData.expandedList + ) val range1 = SearchOccurrence.Range.Key(range = IntRange(2, 2)) val range2 = SearchOccurrence.Range.Key(range = IntRange(4, 4)) @@ -61,6 +71,7 @@ public class JsonTreeSearchTest { actual = result, expected = SearchResult( query = "i", + caseSensitive = false, occurrences = mapOf(4 to occurrence), selectedOccurrence = SelectedSearchOccurrence( occurrence = occurrence, @@ -74,7 +85,11 @@ public class JsonTreeSearchTest { @Test public fun query_with_one_occurrence_in_value_should_return_result_with_one_occurrence_in_value(): TestResult = runTest { - val result = underTest.search(searchQuery = "value1", jsonTreeList = TestData.expandedList) + val result = underTest.search( + searchQuery = "value1", + caseSensitive = false, + jsonTreeList = TestData.expandedList + ) val range = SearchOccurrence.Range.Value(range = IntRange(0, 5)) val occurrence = SearchOccurrence( @@ -85,6 +100,7 @@ public class JsonTreeSearchTest { actual = result, expected = SearchResult( query = "value1", + caseSensitive = false, occurrences = mapOf(2 to occurrence), selectedOccurrence = SelectedSearchOccurrence( occurrence = occurrence, @@ -98,7 +114,11 @@ public class JsonTreeSearchTest { @Test public fun query_with_multiple_occurrences_in_value_should_return_result_with_multiple_occurrence_in_value(): TestResult = runTest { - val result = underTest.search(searchQuery = "second", jsonTreeList = TestData.expandedList) + val result = underTest.search( + searchQuery = "second", + caseSensitive = false, + jsonTreeList = TestData.expandedList + ) val range1 = SearchOccurrence.Range.Value(range = IntRange(0, 5)) val range2 = SearchOccurrence.Range.Value(range = IntRange(6, 11)) @@ -110,6 +130,7 @@ public class JsonTreeSearchTest { actual = result, expected = SearchResult( query = "second", + caseSensitive = false, occurrences = mapOf(4 to occurrence), selectedOccurrence = SelectedSearchOccurrence( occurrence = occurrence, @@ -123,7 +144,11 @@ public class JsonTreeSearchTest { @Test public fun query_with_occurrences_in_key_and_value_should_return_result_with_occurrences_in_key_and_value(): TestResult = runTest { - val result = underTest.search(searchQuery = "r", jsonTreeList = TestData.expandedList) + val result = underTest.search( + searchQuery = "r", + caseSensitive = false, + jsonTreeList = TestData.expandedList + ) val range1 = SearchOccurrence.Range.Key(range = IntRange(1, 1)) val range2 = SearchOccurrence.Range.Key(range = IntRange(2, 2)) @@ -139,6 +164,7 @@ public class JsonTreeSearchTest { actual = result, expected = SearchResult( query = "r", + caseSensitive = false, occurrences = mapOf(1 to occurrence, 4 to occurrence2), selectedOccurrence = SelectedSearchOccurrence( occurrence = occurrence, @@ -150,6 +176,65 @@ public class JsonTreeSearchTest { ) } + @Test + public fun case_sensitive_search_should_return_only_case_matching_results(): TestResult = runTest { + val result = underTest.search( + searchQuery = "A", + caseSensitive = true, + jsonTreeList = listOf(TestData.array1.copy(key = "Array1")) + ) + + val range = SearchOccurrence.Range.Key(range = IntRange(0, 0)) + val occurrence = SearchOccurrence( + listIndex = 0, + ranges = listOf(range) + ) + assertEquals( + actual = result, + expected = SearchResult( + query = "A", + caseSensitive = true, + occurrences = mapOf(0 to occurrence), + selectedOccurrence = SelectedSearchOccurrence( + occurrence = occurrence, + range = range + ), + selectedResultIndex = 0, + totalResults = 1 + ) + ) + } + + @Test + public fun case_insensitive_search_should_return_all_matching_results(): TestResult = runTest { + val result = underTest.search( + searchQuery = "A", + caseSensitive = false, + jsonTreeList = listOf(TestData.array1.copy(key = "Array1")) + ) + + val range = SearchOccurrence.Range.Key(range = IntRange(0, 0)) + val range2 = SearchOccurrence.Range.Key(range = IntRange(3, 3)) + val occurrence = SearchOccurrence( + listIndex = 0, + ranges = listOf(range, range2) + ) + assertEquals( + actual = result, + expected = SearchResult( + query = "A", + caseSensitive = false, + occurrences = mapOf(0 to occurrence), + selectedOccurrence = SelectedSearchOccurrence( + occurrence = occurrence, + range = range + ), + selectedResultIndex = 0, + totalResults = 2 + ) + ) + } + private object TestData { val primitive1 = JsonTreeElement.Primitive( id = "primitive1", diff --git a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/SearchStateTest.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/SearchStateTest.kt index e5715d9..c55c654 100644 --- a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/SearchStateTest.kt +++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/SearchStateTest.kt @@ -31,6 +31,15 @@ public class SearchStateTest { assertEquals(actual = searchState.query, expected = "test") } + @Test + public fun updating_the_case_sensitivity_in_the_state_should_update_the_caseSensitive_variable(): TestResult = runTest { + val searchState = SearchState(defaultDispatcher = dispatcher, mainDispatcher = dispatcher) + assertEquals(actual = searchState.caseSensitive, expected = false) + + searchState.state = searchState.state.copy(caseSensitive = true) + assertEquals(actual = searchState.caseSensitive, expected = true) + } + @Test public fun updating_the_resultCount_in_the_state_should_update_the_resultCount_variable(): TestResult = runTest { val searchState = SearchState(defaultDispatcher = dispatcher, mainDispatcher = dispatcher) @@ -54,6 +63,7 @@ public class SearchStateTest { val searchState = SearchState(defaultDispatcher = dispatcher, mainDispatcher = dispatcher) val searchResult = SearchResult( query = "test", + caseSensitive = true, occurrences = mapOf(0 to SearchOccurrence(0, emptyList())), selectedOccurrence = SelectedSearchOccurrence( occurrence = SearchOccurrence(0, emptyList()), @@ -219,6 +229,7 @@ public class SearchStateTest { private val initialSearchResult = SearchResult( query = null, + caseSensitive = false, occurrences = emptyMap(), selectedOccurrence = null, selectedResultIndex = null, @@ -239,6 +250,7 @@ public class SearchStateTest { private val resultWithOccurrences = SearchResult( query = "t", + caseSensitive = false, occurrences = mapOf(0 to searchOccurrence, 1 to searchOccurrence2), selectedOccurrence = SelectedSearchOccurrence( occurrence = searchOccurrence, diff --git a/shared/src/commonMain/composeResources/drawable/match_case.xml b/shared/src/commonMain/composeResources/drawable/match_case.xml new file mode 100644 index 0000000..3260453 --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/match_case.xml @@ -0,0 +1,9 @@ + + + diff --git a/shared/src/commonMain/kotlin/com/sebastianneubauer/jsontreesample/App.kt b/shared/src/commonMain/kotlin/com/sebastianneubauer/jsontreesample/App.kt index 6a1f45b..acf3b25 100644 --- a/shared/src/commonMain/kotlin/com/sebastianneubauer/jsontreesample/App.kt +++ b/shared/src/commonMain/kotlin/com/sebastianneubauer/jsontreesample/App.kt @@ -57,6 +57,7 @@ import com.sebastianneubauer.jsontree.search.rememberSearchState import com.sebastianneubauer.jsontreesample.shared.generated.resources.Res import com.sebastianneubauer.jsontreesample.shared.generated.resources.arrow_down import com.sebastianneubauer.jsontreesample.shared.generated.resources.arrow_up +import com.sebastianneubauer.jsontreesample.shared.generated.resources.match_case import com.sebastianneubauer.jsontreesample.ui.theme.JsonTreeTheme import kotlinx.coroutines.launch import org.jetbrains.compose.resources.painterResource @@ -252,6 +253,17 @@ private fun MainScreen() { modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically, ) { + IconButton( + onClick = { + coroutineScope.launch { searchState.caseSensitive = !searchState.caseSensitive } + }, + ) { + Icon( + painter = painterResource(Res.drawable.match_case), + contentDescription = "case-sensitive", + ) + } + IconButton( onClick = { coroutineScope.launch { searchState.selectNext() } From ceddf62753ac644456acd9fbe30b36f347534fb0 Mon Sep 17 00:00:00 2001 From: Sebastian Neubauer Date: Fri, 4 Sep 2026 14:39:55 +0200 Subject: [PATCH 2/2] Update API dump --- jsontree/api/jvm/jsontree.api | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jsontree/api/jvm/jsontree.api b/jsontree/api/jvm/jsontree.api index 6a167ce..c72c50e 100644 --- a/jsontree/api/jvm/jsontree.api +++ b/jsontree/api/jvm/jsontree.api @@ -157,12 +157,14 @@ public final class com/sebastianneubauer/jsontree/diff/JsonTreeDiffSuccess { public final class com/sebastianneubauer/jsontree/search/SearchState { public static final field $stable I + public final fun getCaseSensitive ()Z public final fun getQuery ()Ljava/lang/String; public final fun getSelectedResultIndex ()Ljava/lang/Integer; public final fun getSelectedResultListIndex ()Ljava/lang/Integer; public final fun getTotalResults ()I public final fun selectNext (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun selectPrevious (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun setCaseSensitive (Z)V public final fun setQuery (Ljava/lang/String;)V }