Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions jsontree/api/jvm/jsontree.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,15 @@ internal class JsonTreeSearch(
) {
suspend fun search(
searchQuery: String,
caseSensitive: Boolean,
jsonTreeList: List<JsonTreeElement>,
): 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 ->
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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].
*/
Expand Down Expand Up @@ -172,6 +181,7 @@ public class SearchState internal constructor(
internal fun reset() {
state = SearchResult(
query = null,
caseSensitive = false,
occurrences = emptyMap(),
selectedOccurrence = null,
totalResults = 0,
Expand All @@ -182,6 +192,7 @@ public class SearchState internal constructor(
@Immutable
internal data class SearchResult(
val query: String?,
val caseSensitive: Boolean,
val occurrences: Map<Int, SearchOccurrence>,
val selectedOccurrence: SelectedSearchOccurrence?,
val totalResults: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand All @@ -35,6 +40,7 @@ public class JsonTreeSearchTest {
actual = result,
expected = SearchResult(
query = "array1",
caseSensitive = false,
occurrences = mapOf(1 to occurrence),
selectedOccurrence = SelectedSearchOccurrence(
occurrence = occurrence,
Expand All @@ -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))
Expand All @@ -61,6 +71,7 @@ public class JsonTreeSearchTest {
actual = result,
expected = SearchResult(
query = "i",
caseSensitive = false,
occurrences = mapOf(4 to occurrence),
selectedOccurrence = SelectedSearchOccurrence(
occurrence = occurrence,
Expand All @@ -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(
Expand All @@ -85,6 +100,7 @@ public class JsonTreeSearchTest {
actual = result,
expected = SearchResult(
query = "value1",
caseSensitive = false,
occurrences = mapOf(2 to occurrence),
selectedOccurrence = SelectedSearchOccurrence(
occurrence = occurrence,
Expand All @@ -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))
Expand All @@ -110,6 +130,7 @@ public class JsonTreeSearchTest {
actual = result,
expected = SearchResult(
query = "second",
caseSensitive = false,
occurrences = mapOf(4 to occurrence),
selectedOccurrence = SelectedSearchOccurrence(
occurrence = occurrence,
Expand All @@ -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))
Expand All @@ -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,
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()),
Expand Down Expand Up @@ -219,6 +229,7 @@ public class SearchStateTest {

private val initialSearchResult = SearchResult(
query = null,
caseSensitive = false,
occurrences = emptyMap(),
selectedOccurrence = null,
selectedResultIndex = null,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M131,708L296,268L375,268L540,708L464,708L425,596L247,596L207,708L131,708ZM270,532L401,532L337,350L333,350L270,532ZM665,718Q614,718 584,690.5Q554,663 554,618Q554,574 588.5,545.5Q623,517 677,517Q700,517 722,521Q744,525 760,532L760,520Q760,491 739.5,473Q719,455 685,455Q662,455 643,464.5Q624,474 610,492L563,457Q587,428 617.5,414Q648,400 686,400Q755,400 789,432.5Q823,465 823,530L823,708L760,708L760,671L756,671Q742,694 718,706Q694,718 665,718ZM677,664Q712,664 736.5,640Q761,616 761,584Q747,576 727.5,571.5Q708,567 689,567Q657,567 639,581Q621,595 621,618Q621,638 637,651Q653,664 677,664Z"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() }
Expand Down