Skip to content
Open
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
4 changes: 4 additions & 0 deletions platform/jewel/foundation/api-dump.txt
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,10 @@ f:org.jetbrains.jewel.foundation.search.SpeedSearchMatcher$MatchResult$NoMatch
- org.jetbrains.jewel.foundation.search.SpeedSearchMatcher$MatchResult
- sf:$stable:I
- sf:INSTANCE:org.jetbrains.jewel.foundation.search.SpeedSearchMatcher$MatchResult$NoMatch
f:org.jetbrains.jewel.foundation.search.SpeedSearchMatcherKt
- sf:doesMatch(org.jetbrains.jewel.foundation.search.SpeedSearchMatcher,java.lang.String):Z
- sf:filter(java.lang.Iterable,org.jetbrains.jewel.foundation.search.SpeedSearchMatcher):java.util.List
- sf:filter(java.lang.Iterable,org.jetbrains.jewel.foundation.search.SpeedSearchMatcher,kotlin.jvm.functions.Function1):java.util.List
f:org.jetbrains.jewel.foundation.state.CommonStateBitMask
- sf:$stable:I
- sf:FIRST_AVAILABLE_OFFSET:I
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ public fun SelectableLazyColumn(
LaunchedEffect(container, state.selectedKeys) {
val selectedKeysSnapshot = state.selectedKeys
val indices = selectedKeysSnapshot.mapNotNull { key -> container.getKeyIndex(key) }
if (indices != lastEmittedIndices) {
// The `state.lastActiveItemIndex` can also be changed by the SpeedSearch flow.
// If the last active index is not among the selected indices, the SpeedSearch
// has selected a different value and an update must be triggered.
if (indices != lastEmittedIndices || (indices.isNotEmpty() && state.lastActiveItemIndex !in indices)) {
lastEmittedIndices = indices

// Keep keyboard navigation gate in sync after key→index remaps, including through empty→non‑empty
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.foundation.search

import org.jetbrains.annotations.ApiStatus
import org.jetbrains.jewel.foundation.GenerateDataFunctions
import org.jetbrains.jewel.foundation.InternalJewelApi
import org.jetbrains.jewel.foundation.search.impl.ExactSubstringSpeedSearchMatcher
import org.jetbrains.jewel.foundation.search.impl.PatternSpeedSearchMatcher

Expand Down Expand Up @@ -153,6 +155,101 @@ public enum class MatchingCaseSensitivity {
All,
}

/**
* A [SpeedSearchMatcher] implementation that never matches any text.
*
* This matcher is used internally as a performance optimization when the search pattern is empty or invalid. Instead of
* creating a full matcher that would never match anything, this singleton provides a consistent [MatchResult.NoMatch]
* response for all inputs.
*
* **Internal API:** This object is automatically used by `SpeedSearchState` when the filter text is empty. Users should
* not instantiate or use this matcher directly. Instead, use [SpeedSearchMatcher.patternMatcher] or
* [SpeedSearchMatcher.exactSubstringMatcher] to create matchers with actual patterns.
*
* @see SpeedSearchMatcher for creating matchers with actual patterns
* @see filter for filtering collections that handle this matcher efficiently
*/
@InternalJewelApi
@ApiStatus.Internal
public object EmptySpeedSearchMatcher : SpeedSearchMatcher {
override fun matches(text: String?): SpeedSearchMatcher.MatchResult = SpeedSearchMatcher.MatchResult.NoMatch
}

/**
* Checks whether the given text matches the current [SpeedSearchMatcher] pattern.
*
* This is a convenience method that simplifies checking for matches by returning a boolean instead of requiring pattern
* matching on [SpeedSearchMatcher.MatchResult].
*
* Example:
* ```kotlin
* val matcher = SpeedSearchMatcher.patternMatcher("foo")
* matcher.doesMatch("foobar") // true
* matcher.doesMatch("baz") // false
* ```
*
* @param text The text to check for matches. If null, returns false.
* @return `true` if the text matches the pattern, `false` otherwise.
* @see SpeedSearchMatcher.matches for the underlying match result with ranges
*/
public fun SpeedSearchMatcher.doesMatch(text: String?): Boolean =
matches(text) != SpeedSearchMatcher.MatchResult.NoMatch

/**
* Filters an iterable collection based on whether items match the given [SpeedSearchMatcher].
*
* For each item in the collection, the [stringBuilder] function is used to extract a string representation, which is
* then matched against the [matcher]. Items that match are included in the returned list.
*
* If the [matcher] is [EmptySpeedSearchMatcher], all items are returned without filtering, optimizing the common case
* of an empty search query.
*
* Example:
* ```kotlin
* data class User(val name: String, val email: String)
* val users = listOf(
* User("John Doe", "[email protected]"),
* User("Jane Smith", "[email protected]")
* )
*
* val matcher = SpeedSearchMatcher.patternMatcher("john")
* val filtered = users.filter(matcher) { it.name }
* // Returns: [User("John Doe", "[email protected]")]
* ```
*
* @param T The type of items in the collection.
* @param matcher The [SpeedSearchMatcher] to use for filtering.
* @param stringBuilder A function that extracts a string representation from each item for matching.
* @return A list containing only the items that match the search pattern.
* @see doesMatch for the underlying boolean match check
*/
public fun <T> Iterable<T>.filter(matcher: SpeedSearchMatcher, stringBuilder: (T) -> String): List<T> =
if (matcher is EmptySpeedSearchMatcher) {
toList()
} else {
filter { matcher.doesMatch(stringBuilder(it)) }
}

/**
* Filters an iterable collection of strings based on the given [SpeedSearchMatcher].
*
* This is a convenience overload of [filter] for working directly with string collections, eliminating the need to
* provide a string extraction function.
*
* Example:
* ```kotlin
* val frameworks = listOf("React", "Vue.js", "Angular", "Svelte")
* val matcher = SpeedSearchMatcher.patternMatcher("react")
* val filtered = frameworks.filter(matcher)
* // Returns: ["React"]
* ```
*
* @param matcher The [SpeedSearchMatcher] to use for filtering.
* @return A list containing only the strings that match the search pattern.
* @see filter for filtering collections of other types
*/
public fun Iterable<String>.filter(matcher: SpeedSearchMatcher): List<String> = filter(matcher) { it }

/**
* Split the input into words based on case changes, digits, and special characters, and join them with the wildcard
* ('*') character.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.foundation.search

import org.junit.Assert.assertEquals
import org.junit.Test

public class IterableFilterTest {
// Test data classes
private data class User(val name: String, val email: String)

// Exact substring matcher tests
@Test
public fun `filter strings should return only exact matching items`() {
val items = listOf("apple", "pineapple", "application", "banana")
val matcher = SpeedSearchMatcher.exactSubstringMatcher("apple")
val result = items.filter(matcher)

assertEquals(2, result.size)
assertEquals(listOf("apple", "pineapple"), result)
}

@Test
public fun `filter strings should return empty list when no items match`() {
val items = listOf("apple", "banana", "cherry")
val matcher = SpeedSearchMatcher.exactSubstringMatcher("xyz")
val result = items.filter(matcher)

assertEquals(0, result.size)
}

@Test
public fun `filter should work with custom types`() {
val users =
listOf(
User("John Doe", "[email protected]"),
User("Jane Smith", "[email protected]"),
User("Johnny Cash", "[email protected]"),
)
val matcher = SpeedSearchMatcher.exactSubstringMatcher("jane")
val result = users.filter(matcher) { it.name }

assertEquals(1, result.size)
assertEquals(listOf(users[1]), result)
}

@Test
public fun `filter return empty list when no items match with custom types`() {
val users =
listOf(
User("John Doe", "[email protected]"),
User("Jane Smith", "[email protected]"),
User("Johnny Cash", "[email protected]"),
)
val matcher = SpeedSearchMatcher.exactSubstringMatcher("Joana")
val result = users.filter(matcher) { it.name }

assertEquals(0, result.size)
}
}
Loading
Loading