[platform UI] IJPL-209442 Search Everywhere: show "No exact matches found. Here are similar results" hint in the search field when only semantic search results are listed

GitOrigin-RevId: 6eccd43bb3fb88a9a618df592974917a4926aa36
This commit is contained in:
Tatiana Elfimova
2025-11-18 13:06:22 +00:00
committed by intellij-monorepo-bot
parent d1782aa310
commit 30a4143f47
4 changed files with 87 additions and 10 deletions
@@ -24,6 +24,7 @@ import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.ui.popup.ListItemDescriptorAdapter
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.text.StringUtil
import com.intellij.platform.searchEverywhere.*
import com.intellij.platform.searchEverywhere.data.SeDataKeys
import com.intellij.platform.searchEverywhere.frontend.AutoToggleAction
@@ -122,6 +123,8 @@ class SePopupContentPane(
private set
var popupExtendedSize: Dimension? = initPopupExtendedSize
private val semanticWarning = MutableStateFlow(false)
init {
layout = GridLayout()
@@ -265,6 +268,7 @@ class SePopupContentPane(
updateEmptyStatus()
}
semanticWarning.value = resultListModel.isValidAndHasOnlySemantic
updateViewMode()
autoSelectIndex(searchContext.searchPattern, true)
}
@@ -345,9 +349,12 @@ class SePopupContentPane(
}
launch {
vm.searchFieldHint.collect { hint ->
combine(vm.searchFieldHint, semanticWarning) { searchFieldHint, semanticWarning ->
searchFieldHint to semanticWarning
}.collect { (searchFieldHint, semanticWarning) ->
withContext(Dispatchers.EDT) {
hint.let { (text, tooltip, isWarning) ->
if (searchFieldHint != null) {
val (text, tooltip, isWarning) = searchFieldHint
if (isWarning) {
hintHelper.setLoadingText(text, tooltip)
}
@@ -355,6 +362,15 @@ class SePopupContentPane(
hintHelper.setHint(text)
}
}
else if (semanticWarning) {
val noExactMatchesText = SemanticBundleProvider.getSemanticBundle()?.getMessage("search.everywhere.no.exact.matches")?.let {
StringUtil.trimTrailing(it, ':')
}
hintHelper.setHint(noExactMatchesText)
}
else {
hintHelper.setHint(null)
}
}
}
}
@@ -4,18 +4,22 @@ package com.intellij.platform.searchEverywhere.frontend.ui
import com.intellij.ide.rpc.ThrottledAccumulatedItems
import com.intellij.ide.rpc.ThrottledItems
import com.intellij.ide.rpc.ThrottledOneItem
import com.intellij.platform.searchEverywhere.SeItemDataKeys
import com.intellij.platform.searchEverywhere.SeResultAddedEvent
import com.intellij.platform.searchEverywhere.SeResultEvent
import com.intellij.platform.searchEverywhere.SeResultReplacedEvent
import com.intellij.platform.searchEverywhere.frontend.SeSearchStatePublisher
import com.intellij.platform.searchEverywhere.frontend.vm.SeSearchContext
import com.intellij.platform.searchEverywhere.providers.SeLog
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.ApiStatus.Internal
import javax.swing.DefaultListModel
import javax.swing.ListSelectionModel
import kotlin.text.toBoolean
@ApiStatus.Internal
@Internal
class SeResultListModel(private val searchStatePublisher: SeSearchStatePublisher,
private val selectionModelProvider: () -> ListSelectionModel): DefaultListModel<SeResultListRow>() {
val freezer: Freezer = Freezer { size }
@@ -26,16 +30,21 @@ class SeResultListModel(private val searchStatePublisher: SeSearchStatePublisher
val pendingReplacementElementUuids: MutableSet<String> = mutableSetOf()
var isValidAndHasOnlySemantic: Boolean = false
private set
fun reset() {
SeLog.log(SeLog.THROTTLING) { "Will reset result list model" }
freezer.reset()
_isValidState.value = true
pendingReplacementElementUuids.clear()
removeAllElements()
isValidAndHasOnlySemantic = false
}
fun invalidate() {
_isValidState.value = false
isValidAndHasOnlySemantic = false
}
fun removeLoadingItem() {
@@ -47,6 +56,8 @@ class SeResultListModel(private val searchStatePublisher: SeSearchStatePublisher
fun addFromThrottledEvent(searchContext: SeSearchContext, throttledEvent: ThrottledItems<SeResultEvent>) {
if (!isValid) reset()
val wasEmpty = isEmpty
val resultListAdapter = SeResultListModelAdapter(this, selectionModelProvider())
when (throttledEvent) {
is ThrottledAccumulatedItems<SeResultEvent> -> {
@@ -83,6 +94,8 @@ class SeResultListModel(private val searchStatePublisher: SeSearchStatePublisher
})
}
}
isValidAndHasOnlySemantic = (isValidAndHasOnlySemantic || (wasEmpty && !isEmpty)) && throttledEvent.allItemsAreSemantic()
}
class Freezer(private val listSize: () -> Int) {
@@ -109,4 +122,23 @@ class SeResultListModel(private val searchStatePublisher: SeSearchStatePublisher
frozenCountToApply = 0
}
}
}
}
private fun <T : SeResultEvent> ThrottledItems<T>.allItemsAreSemantic(): Boolean =
when (this) {
is ThrottledAccumulatedItems<T> -> items.all { it.isSemantic() }
is ThrottledOneItem<T> -> item.isSemantic()
}
private fun SeResultEvent.isSemantic(): Boolean {
val itemData = when (this) {
is SeResultAddedEvent -> {
itemData
}
is SeResultReplacedEvent -> {
newItemData
}
else -> null
}
return itemData?.additionalInfo?.get(SeItemDataKeys.IS_SEMANTIC).toBoolean()
}
@@ -0,0 +1,30 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.searchEverywhere.frontend.ui
import com.intellij.DynamicBundle
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.extensions.PluginId
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
object SemanticBundleProvider {
fun getSemanticBundle(): DynamicBundle? {
val pluginId = PluginId.getId("com.intellij.ml.llm")
if (!PluginManagerCore.isPluginInstalled(pluginId)) {
return null
}
val descriptor = PluginManagerCore.getPlugin(pluginId) ?: return null
val classLoader = descriptor.pluginClassLoader
return try {
val clazz = classLoader?.loadClass(
"com.intellij.ml.llm.searchEverywhere.embeddings.SemanticSearchBundle"
)
val instanceField = clazz?.getDeclaredField("INSTANCE")
instanceField?.get(null) as DynamicBundle
} catch (_: Throwable) {
null
}
}
}
@@ -66,8 +66,8 @@ class SePopupVm(
data class SearchFieldHint(val text: String?, val tooltip: String?, val isWarning: Boolean)
private val _searchFieldHint = MutableStateFlow(SearchFieldHint("", null, false))
val searchFieldHint: StateFlow<SearchFieldHint> = _searchFieldHint
private val _searchFieldHint = MutableStateFlow<SearchFieldHint?>(null)
val searchFieldHint: StateFlow<SearchFieldHint?> = _searchFieldHint
private var historyIterator: HistoryIterator = historyList.getIterator(currentTab.tabId)
get() {
@@ -151,11 +151,10 @@ class SePopupVm(
SearchFieldHint(IdeBundle.message("incomplete.mode.results.might.be.incomplete"), null, true)
}
else {
val text = if (currentTab.isCommandsSupported()) {
IdeBundle.message("searcheverywhere.textfield.hint", getTopHitAccelerator())
if (currentTab.isCommandsSupported()) {
SearchFieldHint(IdeBundle.message("searcheverywhere.textfield.hint", getTopHitAccelerator()), null, false)
}
else null
SearchFieldHint(text, null, false)
}
}.distinctUntilChanged().collect {
_searchFieldHint.value = it