diff --git a/platform/searchEverywhere/frontend/src/ui/SePopupContentPane.kt b/platform/searchEverywhere/frontend/src/ui/SePopupContentPane.kt index 28ba216879a4..0a7552b1ff0d 100644 --- a/platform/searchEverywhere/frontend/src/ui/SePopupContentPane.kt +++ b/platform/searchEverywhere/frontend/src/ui/SePopupContentPane.kt @@ -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) + } } } } diff --git a/platform/searchEverywhere/frontend/src/ui/SeResultListModel.kt b/platform/searchEverywhere/frontend/src/ui/SeResultListModel.kt index 1a9fca6c7adb..b790f80f3539 100644 --- a/platform/searchEverywhere/frontend/src/ui/SeResultListModel.kt +++ b/platform/searchEverywhere/frontend/src/ui/SeResultListModel.kt @@ -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() { val freezer: Freezer = Freezer { size } @@ -26,16 +30,21 @@ class SeResultListModel(private val searchStatePublisher: SeSearchStatePublisher val pendingReplacementElementUuids: MutableSet = 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) { if (!isValid) reset() + val wasEmpty = isEmpty + val resultListAdapter = SeResultListModelAdapter(this, selectionModelProvider()) when (throttledEvent) { is ThrottledAccumulatedItems -> { @@ -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 } } -} \ No newline at end of file +} + +private fun ThrottledItems.allItemsAreSemantic(): Boolean = + when (this) { + is ThrottledAccumulatedItems -> items.all { it.isSemantic() } + is ThrottledOneItem -> 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() +} diff --git a/platform/searchEverywhere/frontend/src/ui/SemanticBundleProvider.kt b/platform/searchEverywhere/frontend/src/ui/SemanticBundleProvider.kt new file mode 100644 index 000000000000..795555511f4e --- /dev/null +++ b/platform/searchEverywhere/frontend/src/ui/SemanticBundleProvider.kt @@ -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 + } + } +} diff --git a/platform/searchEverywhere/frontend/src/vm/SePopupVm.kt b/platform/searchEverywhere/frontend/src/vm/SePopupVm.kt index 7a4424d25af2..840eca825693 100644 --- a/platform/searchEverywhere/frontend/src/vm/SePopupVm.kt +++ b/platform/searchEverywhere/frontend/src/vm/SePopupVm.kt @@ -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 + private val _searchFieldHint = MutableStateFlow(null) + val searchFieldHint: StateFlow = _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