[platform UI] IJPL-206730 Search Everywhere for RD: Support commands via "/"

GitOrigin-RevId: fc01068a606b2841cf0a5e4ea97cbf15b24b7e38
This commit is contained in:
Tatiana Elfimova
2025-11-13 12:27:40 +00:00
committed by intellij-monorepo-bot
parent 4c04714771
commit 1dee6dc99c
30 changed files with 369 additions and 70 deletions
@@ -271,6 +271,18 @@ class SeBackendService(val project: Project, private val coroutineScope: Corouti
}
}
suspend fun isCommandsSupported(
session: SeSession,
dataContextId: DataContextId,
providerIds: List<SeProviderId>,
isAllTab: Boolean,
): Boolean {
return providerIds.any { providerId ->
val provider = getProvidersHolder(session, dataContextId)?.get(providerId, isAllTab)
provider?.isCommandsSupported() ?: false
}
}
companion object {
@JvmStatic
fun getInstance(project: Project): SeBackendService = project.service<SeBackendService>()
@@ -154,4 +154,9 @@ class SeRemoteApiImpl : SeRemoteApi {
val project = projectId.findProjectOrNull() ?: return false
return SeBackendService.getInstance(project).isExtendedInfoEnabled(session, dataContextId, providerIds, isAllTab)
}
override suspend fun isCommandsSupported(projectId: ProjectId, session: SeSession, dataContextId: DataContextId, providerIds: List<SeProviderId>, isAllTab: Boolean): Boolean {
val project = projectId.findProjectOrNull() ?: return false
return SeBackendService.getInstance(project).isCommandsSupported(session, dataContextId, providerIds, isAllTab)
}
}
@@ -33,7 +33,7 @@ class SeRunConfigurationsItem(
}
@ApiStatus.Internal
class SeRunConfigurationsProvider(private val contributorWrapper: SeAsyncContributorWrapper<Any>) : SeWrappedLegacyContributorItemsProvider() {
class SeRunConfigurationsProvider(private val contributorWrapper: SeAsyncContributorWrapper<Any>) : SeWrappedLegacyContributorItemsProvider(), SeCommandsProviderInterface {
override val contributor: SearchEverywhereContributor<Any> get() = contributorWrapper.contributor
override val id: String get() = SeProviderIdUtils.RUN_CONFIGURATIONS_ID
override val displayName: @Nls String get() = contributor.fullGroupName
@@ -61,6 +61,10 @@ class SeRunConfigurationsProvider(private val contributorWrapper: SeAsyncContrib
return contributor.showInFindResults()
}
override fun getSupportedCommands(): List<SeCommandInfo> {
return contributor.supportedCommands.map { commandInfo -> SeCommandInfo(commandInfo, id) }
}
override fun dispose() {
Disposer.dispose(contributorWrapper)
}
@@ -103,6 +103,12 @@ class SeFrontendItemDataProvidersFacade(private val projectId: ProjectId,
)
}
suspend fun isCommandsSupported(): Boolean {
return SeRemoteApi.getInstance().isCommandsSupported(
projectId, providerIds = providerIds, session = session, dataContextId = dataContextId, isAllTab = isAllTab
)
}
companion object {
private const val DEFAULT_CHUNK_SIZE: Int = 50
}
@@ -0,0 +1,14 @@
// 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
import org.jetbrains.annotations.ApiStatus.Internal
@Internal
sealed interface SeSelectionResult
@Internal
class SeSelectionResultClose : SeSelectionResult
@Internal
class SeSelectionResultKeep : SeSelectionResult
@Internal
class SeSelectionResultText(val searchText: String) : SeSelectionResult
@@ -49,4 +49,6 @@ interface SeTab : Disposable {
suspend fun getPreviewInfo(itemData: SeItemData): SePreviewInfo?
suspend fun isExtendedInfoEnabled(): Boolean
suspend fun isCommandsSupported(): Boolean
}
@@ -26,6 +26,9 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.Nls
import kotlin.collections.flatMap
import kotlin.collections.plus
import kotlin.collections.toSet
@OptIn(ExperimentalCoroutinesApi::class)
@Internal
@@ -167,6 +170,10 @@ class SeTabDelegate(
return providers.getValue().isExtendedInfoEnabled()
}
suspend fun isCommandsSupported(): Boolean {
return providers.getValue().isCommandsSupported()
}
override fun dispose() {}
private class Providers(
@@ -274,6 +281,10 @@ class SeTabDelegate(
suspend fun isExtendedInfoEnabled(): Boolean {
return localProviders.values.any { it.isExtendedInfoEnabled() } || frontendProvidersFacade?.isExtendedInfoEnabled() == true
}
suspend fun isCommandsSupported(): Boolean {
return localProviders.values.any { it.isCommandsSupported() } || frontendProvidersFacade?.isCommandsSupported() == true
}
}
// Workaround for: IJPL-188383 Search Everywhere, All tab: 'Top Hit' filter is duplicated
@@ -68,6 +68,10 @@ class SeActionsTab(private val delegate: SeTabDelegate) : SeTab {
return delegate.isExtendedInfoEnabled()
}
override suspend fun isCommandsSupported(): Boolean {
return delegate.isCommandsSupported()
}
override fun dispose() {
Disposer.dispose(delegate)
}
@@ -82,6 +82,10 @@ class SeAllTab(private val delegate: SeTabDelegate) : SeTab {
return delegate.getPreviewInfo(itemData, true)
}
override suspend fun isCommandsSupported(): Boolean {
return delegate.isCommandsSupported()
}
override fun dispose() {
Disposer.dispose(delegate)
}
@@ -69,6 +69,10 @@ class SeClassesTab(private val delegate: SeTabDelegate) : SeTab {
return delegate.isExtendedInfoEnabled()
}
override suspend fun isCommandsSupported(): Boolean {
return delegate.isCommandsSupported()
}
override fun dispose() {
Disposer.dispose(delegate)
}
@@ -70,6 +70,10 @@ class SeFilesTab(private val delegate: SeTabDelegate) : SeTab {
return delegate.isExtendedInfoEnabled()
}
override suspend fun isCommandsSupported(): Boolean {
return delegate.isCommandsSupported()
}
override fun dispose() {
Disposer.dispose(delegate)
}
@@ -49,6 +49,10 @@ class SeTabMock(
return delegate.isExtendedInfoEnabled()
}
override suspend fun isCommandsSupported(): Boolean {
return delegate.isCommandsSupported()
}
override fun dispose() {
Disposer.dispose(delegate)
}
@@ -69,6 +69,10 @@ open class SeSymbolsTab(private val delegate: SeTabDelegate) : SeTab {
return delegate.isExtendedInfoEnabled()
}
override suspend fun isCommandsSupported(): Boolean {
return delegate.isCommandsSupported()
}
override fun dispose() {
Disposer.dispose(delegate)
}
@@ -72,6 +72,10 @@ class SeTextTab(private val delegate: SeTabDelegate, registerShortcut: (AnAction
return delegate.isExtendedInfoEnabled()
}
override suspend fun isCommandsSupported(): Boolean {
return delegate.isCommandsSupported()
}
override fun dispose() {
Disposer.dispose(filterEditorDisposable)
Disposer.dispose(delegate)
@@ -30,6 +30,8 @@ import com.intellij.platform.searchEverywhere.frontend.AutoToggleAction
import com.intellij.platform.searchEverywhere.frontend.SeSearchStatePublisher
import com.intellij.platform.searchEverywhere.frontend.SeSelectionListener
import com.intellij.platform.searchEverywhere.frontend.SeSelectionState
import com.intellij.platform.searchEverywhere.frontend.SeSelectionResultClose
import com.intellij.platform.searchEverywhere.frontend.SeSelectionResultText
import com.intellij.platform.searchEverywhere.frontend.tabs.actions.SeActionItemPresentationRenderer
import com.intellij.platform.searchEverywhere.frontend.tabs.all.SeAllTab
import com.intellij.platform.searchEverywhere.frontend.tabs.files.SeTargetItemPresentationRenderer
@@ -271,7 +273,7 @@ class SePopupContentPane(
hintHelper.setSearchInProgress(false)
val wasFrozen = resultListModel.freezer.isEnabled
resultListModel.addFromThrottledEvent(searchId, event)
resultListModel.addFromThrottledEvent(searchContext, event)
// Freeze back if it was frozen before
if (wasFrozen) resultListModel.freezer.enable()
@@ -343,11 +345,16 @@ class SePopupContentPane(
}
launch {
vm.searchFieldWarning.collect { warning ->
vm.searchFieldHint.collect { hint ->
withContext(Dispatchers.EDT) {
warning?.let { (text, tooltip) ->
hintHelper.setLoadingText(text, tooltip)
} ?: hintHelper.setHint(null)
hint.let { (text, tooltip, isWarning) ->
if (isWarning) {
hintHelper.setLoadingText(text, tooltip)
}
else {
hintHelper.setHint(text)
}
}
}
}
}
@@ -488,10 +495,13 @@ class SePopupContentPane(
}
}
if (vmState.value?.itemsSelected(itemDataList, nonItemDataCount == 0, modifiers) == true) {
val selectedItems = vmState.value?.itemsSelected(itemDataList, nonItemDataCount == 0, modifiers)
if (selectedItems?.any { it is SeSelectionResultClose } == true) {
closePopup()
}
else {
(selectedItems?.filterIsInstance<SeSelectionResultText>()?.firstOrNull())?.let { textField.text = it.searchText }
resultList.repaint()
refreshPresentations()
}
@@ -502,7 +512,7 @@ class SePopupContentPane(
val listSize = resultListModel.size
val firstIndex = resultList.firstVisibleIndex.takeIf { it in 0..<listSize } ?: return
val lastIndex = resultList.lastVisibleIndex.takeIf { it in 0..<listSize && it >= firstIndex} ?: return
val lastIndex = resultList.lastVisibleIndex.takeIf { it in 0..<listSize && it >= firstIndex } ?: return
val visibleRows = (firstIndex..lastIndex).mapNotNull {
resultListModel.get(it) as? SeResultListItemRow
@@ -6,6 +6,7 @@ import com.intellij.ide.rpc.ThrottledItems
import com.intellij.ide.rpc.ThrottledOneItem
import com.intellij.platform.searchEverywhere.SeResultEvent
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
@@ -43,7 +44,7 @@ class SeResultListModel(private val searchStatePublisher: SeSearchStatePublisher
}
}
fun addFromThrottledEvent(searchId: String, throttledEvent: ThrottledItems<SeResultEvent>) {
fun addFromThrottledEvent(searchContext: SeSearchContext, throttledEvent: ThrottledItems<SeResultEvent>) {
if (!isValid) reset()
val resultListAdapter = SeResultListModelAdapter(this, selectionModelProvider())
@@ -51,7 +52,7 @@ class SeResultListModel(private val searchStatePublisher: SeSearchStatePublisher
is ThrottledAccumulatedItems<SeResultEvent> -> {
val accumulatedList = SeResultListCollection(pendingReplacementElementUuids)
throttledEvent.items.forEach {
accumulatedList.handleEvent(it)
accumulatedList.handleEvent(searchContext, it)
}
// Remove SeResultListMoreRow from the accumulatedList if we already have one in the real listModel
@@ -71,14 +72,14 @@ class SeResultListModel(private val searchStatePublisher: SeSearchStatePublisher
accumulatedList.list.filterIsInstance<SeResultListItemRow>().takeIf { it.isNotEmpty() }?.map {
it.item
}?.let { items ->
searchStatePublisher.elementsAdded(searchId, items.associateBy { it.uuid })
searchStatePublisher.elementsAdded(searchContext.searchId, items.associateBy { it.uuid })
}
}
is ThrottledOneItem<SeResultEvent> -> {
resultListAdapter.handleEvent(throttledEvent.item, onAdd = {
searchStatePublisher.elementsAdded(searchId, mapOf(it.uuid to it))
resultListAdapter.handleEvent(searchContext, throttledEvent.item, onAdd = {
searchStatePublisher.elementsAdded(searchContext.searchId, mapOf(it.uuid to it))
}, onRemove = {
searchStatePublisher.elementsRemoved(searchId, 1)
searchStatePublisher.elementsRemoved(searchContext.searchId, 1)
})
}
}
@@ -4,6 +4,7 @@ package com.intellij.platform.searchEverywhere.frontend.ui
import com.intellij.ide.actions.searcheverywhere.RecentFilesSEContributor
import com.intellij.openapi.options.advanced.AdvancedSettings
import com.intellij.platform.searchEverywhere.*
import com.intellij.platform.searchEverywhere.frontend.vm.SeSearchContext
import com.intellij.platform.searchEverywhere.providers.SeLog
import com.intellij.platform.searchEverywhere.providers.topHit.SeTopHitItemsProvider
import org.jetbrains.annotations.ApiStatus
@@ -37,14 +38,14 @@ interface SeResultList {
}
@ApiStatus.Internal
fun SeResultList.handleEvent(event: SeResultEvent, onAdd: ((SeItemData) -> Unit)? = null, onRemove: (() -> Unit)? = null) {
fun SeResultList.handleEvent(searchContext: SeSearchContext, event: SeResultEvent, onAdd: ((SeItemData) -> Unit)? = null, onRemove: (() -> Unit)? = null) {
when (event) {
is SeResultAddedEvent -> {
if (pendingReplacementElementUuids.remove(event.itemData.uuid)) {
SeLog.log(SeLog.DEFAULT) { "SeResultAddedEvent: uuid ${event.itemData.uuid} was skipped because it was supposed to be replaced by an element which came earlier" }
}
else {
val index = indexToAdd(event.itemData)
val index = indexToAdd(event.itemData, searchContext.searchPattern)
addRow(index, SeResultListItemRow(event.itemData))
onAdd?.invoke(event.itemData)
@@ -69,7 +70,7 @@ fun SeResultList.handleEvent(event: SeResultEvent, onAdd: ((SeItemData) -> Unit)
}.sortedDescending()
if (indexes.isEmpty()) {
val index = indexToAdd(event.newItemData)
val index = indexToAdd(event.newItemData, searchContext.searchPattern)
addRow(index, SeResultListItemRow(event.newItemData))
onAdd?.invoke(event.newItemData)
}
@@ -85,12 +86,34 @@ fun SeResultList.handleEvent(event: SeResultEvent, onAdd: ((SeItemData) -> Unit)
}
}
}
is SeResultEndEvent -> {}// Do nothing
is SeResultEndEvent -> {} // Do nothing
}
}
private fun SeResultList.indexToAdd(newItem: SeItemData): Int {
private fun SeResultList.indexToAdd(newItem: SeItemData, searchPattern: String): Int {
if (newItem.isCommand) {
val firstNotCommandIndex = firstIndexOrNull(true, true) { item -> !item.isCommand } ?: size
val comparator = compareBy<SeItemData>(
{ !it.presentation.text.lowercase().startsWith(searchPattern) },
{ it.presentation.text.lowercase() }
)
for (i in 0..<firstNotCommandIndex) {
val row = getRow(i)
if (row is SeResultListItemRow) {
val item = row.item
if (comparator.compare(newItem, item) < 0) {
return i
}
}
}
return firstNotCommandIndex
}
return firstIndexOrNull(false) { item ->
if (item.isCommand) return@firstIndexOrNull false
val newItemProviderPriority = SeResultList.prioritizedProvidersPriorities[newItem.providerId] ?: 0
val itemProviderPriority = SeResultList.prioritizedProvidersPriorities[item.providerId] ?: 0
@@ -103,7 +126,7 @@ private fun SeResultList.indexToAdd(newItem: SeItemData): Int {
} ?: lastIndexToInsertItem
}
private fun SeResultList.firstIndexOrNull(fullSearch: Boolean, predicate: (SeItemData) -> Boolean): Int? {
private fun SeResultList.firstIndexOrNull(fullSearch: Boolean, acceptMoreRow: Boolean = false, predicate: (SeItemData) -> Boolean): Int? {
val startIndex = if (fullSearch) 0 else frozenCount
return (startIndex until size).firstOrNull { index ->
@@ -111,7 +134,7 @@ private fun SeResultList.firstIndexOrNull(fullSearch: Boolean, predicate: (SeIte
is SeResultListItemRow -> {
predicate(row.item)
}
SeResultListMoreRow -> false
SeResultListMoreRow -> acceptMoreRow
}
}
}
@@ -2,6 +2,7 @@
package com.intellij.platform.searchEverywhere.frontend.vm
import com.intellij.ide.IdeBundle
import com.intellij.ide.SearchTopHitProvider.Companion.getTopHitAccelerator
import com.intellij.ide.actions.searcheverywhere.*
import com.intellij.ide.actions.searcheverywhere.SEHeaderActionListener.Companion.SE_HEADER_ACTION_TOPIC
import com.intellij.ide.actions.searcheverywhere.SearchEverywhereUI.PREVIEW_EVENTS
@@ -21,6 +22,7 @@ import com.intellij.openapi.wm.ToolWindowManager.Companion.getInstance
import com.intellij.platform.searchEverywhere.SeItemData
import com.intellij.platform.searchEverywhere.SeProviderId
import com.intellij.platform.searchEverywhere.SeSession
import com.intellij.platform.searchEverywhere.frontend.SeSelectionResult
import com.intellij.platform.searchEverywhere.frontend.SeTab
import com.intellij.platform.searchEverywhere.frontend.tabs.actions.SeActionsTab
import com.intellij.platform.searchEverywhere.utils.SuspendLazyProperty
@@ -62,8 +64,10 @@ class SePopupVm(
private val canBeShownInFindResultsFlow = MutableStateFlow(false)
val canBeShownInFindResults: Boolean get() = canBeShownInFindResultsFlow.value
private val _searchFieldWarning = MutableStateFlow<Pair<String, String?>?>(null)
val searchFieldWarning: StateFlow<Pair<String, String?>?> = _searchFieldWarning
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 var historyIterator: HistoryIterator = historyList.getIterator(currentTab.tabId)
get() {
@@ -135,21 +139,26 @@ class SePopupVm(
}.map { (currentTab, isDumb, isIncomplete) ->
// IJPL-193615: In RemDev, IncompleteDependenciesService state is not synchronized between frontend and backend,
// so isIncomplete always remains false on frontend, making dependency loading messages unavailable in RemDev.
if (!currentTab.isIndexingDependent || (!isDumb && !isIncomplete)) return@map null
if (isDumb) {
if (currentTab.isIndexingDependent && isDumb) {
if (currentTab.tabId == SeActionsTab.ID) {
Pair(IdeBundle.message("dumb.mode.analyzing.project"), IdeBundle.message("dumb.mode.some.actions.might.be.unavailable.during.project.analysis"))
SearchFieldHint(IdeBundle.message("dumb.mode.analyzing.project"), IdeBundle.message("dumb.mode.some.actions.might.be.unavailable.during.project.analysis"), true)
}
else {
Pair(IdeBundle.message("dumb.mode.analyzing.project"), IdeBundle.message("dumb.mode.results.might.be.incomplete.during.project.analysis"))
SearchFieldHint(IdeBundle.message("dumb.mode.analyzing.project"), IdeBundle.message("dumb.mode.results.might.be.incomplete.during.project.analysis"), true)
}
}
else if (currentTab.isIndexingDependent && isIncomplete) {
SearchFieldHint(IdeBundle.message("incomplete.mode.results.might.be.incomplete"), null, true)
}
else {
Pair(IdeBundle.message("incomplete.mode.results.might.be.incomplete"), null)
val text = if (currentTab.isCommandsSupported()) {
IdeBundle.message("searcheverywhere.textfield.hint", getTopHitAccelerator())
}
else null
SearchFieldHint(text, null, false)
}
}.distinctUntilChanged().collect {
_searchFieldWarning.value = it
_searchFieldHint.value = it
}
}
@@ -182,7 +191,7 @@ class SePopupVm(
}
}
suspend fun itemsSelected(indexedItems: List<Pair<Int, SeItemData>>, areIndexesOriginal: Boolean, modifiers: Int): Boolean {
suspend fun itemsSelected(indexedItems: List<Pair<Int, SeItemData>>, areIndexesOriginal: Boolean, modifiers: Int): List<SeSelectionResult> {
val currentTab = currentTab
return coroutineScope {
@@ -190,7 +199,7 @@ class SePopupVm(
async {
currentTab.itemSelected(item, areIndexesOriginal, modifiers, searchPattern.value)
}
}.awaitAll().any { it }
}.awaitAll()
}
}
@@ -21,6 +21,10 @@ import com.intellij.platform.searchEverywhere.*
import com.intellij.platform.searchEverywhere.frontend.AutoToggleAction
import com.intellij.platform.searchEverywhere.frontend.SeEmptyResultInfo
import com.intellij.platform.searchEverywhere.frontend.SeFilterEditor
import com.intellij.platform.searchEverywhere.frontend.SeSelectionResult
import com.intellij.platform.searchEverywhere.frontend.SeSelectionResultClose
import com.intellij.platform.searchEverywhere.frontend.SeSelectionResultKeep
import com.intellij.platform.searchEverywhere.frontend.SeSelectionResultText
import com.intellij.platform.searchEverywhere.frontend.SeTab
import com.intellij.platform.searchEverywhere.providers.SeAdaptedItem
import com.intellij.platform.searchEverywhere.providers.SeLog
@@ -32,6 +36,7 @@ import org.jetbrains.annotations.ApiStatus
import java.util.*
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.text.toBoolean
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
@ApiStatus.Internal
@@ -152,9 +157,17 @@ class SeTabVm(
isActiveFlow.value = isActive
}
suspend fun itemSelected(itemWithIndex: Pair<Int, SeItemData>, isIndexOriginal: Boolean, modifiers: Int, searchText: String): Boolean {
suspend fun itemSelected(itemWithIndex: Pair<Int, SeItemData>, isIndexOriginal: Boolean, modifiers: Int, searchText: String): SeSelectionResult {
logItemSelectedEvent(itemWithIndex, isIndexOriginal)
return tab.itemSelected(itemWithIndex.second, modifiers, searchText)
if (itemWithIndex.second.isCommand) {
return SeSelectionResultText(itemWithIndex.second.presentation.text)
}
return if (tab.itemSelected(itemWithIndex.second, modifiers, searchText))
SeSelectionResultClose()
else
SeSelectionResultKeep()
}
private fun logItemSelectedEvent(itemWithIndex: Pair<Int, SeItemData>, isIndexOriginal: Boolean) {
@@ -242,6 +255,10 @@ class SeTabVm(
suspend fun isExtendedInfoEnabled() : Boolean {
return tab.isExtendedInfoEnabled()
}
suspend fun isCommandsSupported(): Boolean {
return tab.isCommandsSupported()
}
}
private const val ESSENTIALS_THROTTLE_DELAY: Long = 100
@@ -258,7 +275,7 @@ private fun Flow<SeResultEvent>.throttleUntilEssentialsArrive(essentialProviderI
resultThrottlingMs = essentialWaitingTimeout,
shouldPassItem = { it !is SeResultEndEvent },
fastPassThrottlingMs = FAST_PASS_THROTTLE,
shouldFastPassItem = { it.providerId().shouldIgnoreThrottling() }
shouldFastPassItem = { it.itemDataOrNull()?.shouldIgnoreThrottling() == true }
) { event: SeResultEvent, _: Int ->
val providerId = event.providerId()
@@ -295,8 +312,12 @@ private fun SeResultEvent.itemDataOrNull(): SeItemData? = when (this) {
is SeResultEndEvent -> null
}
private fun SeProviderId.shouldIgnoreThrottling(): Boolean =
AdvancedSettings.getBoolean("search.everywhere.recent.at.top") && this.value == SeProviderIdUtils.RECENT_FILES_ID
private fun SeItemData.shouldIgnoreThrottling(): Boolean =
AdvancedSettings.getBoolean("search.everywhere.recent.at.top") &&
this.providerId.value == SeProviderIdUtils.RECENT_FILES_ID ||
this.isCommand
@ApiStatus.Internal
class SeSearchContext(val searchId: String, val tabId: String, val searchPattern: String, val resultsFlow: Flow<ThrottledItems<SeResultEvent>>)
class SeSearchContext(val searchId: String, val tabId: String,
val searchPattern: String,
val resultsFlow: Flow<ThrottledItems<SeResultEvent>>)
@@ -0,0 +1,21 @@
// 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
import com.intellij.ide.actions.searcheverywhere.SearchEverywhereCommandInfo
import kotlinx.serialization.Serializable
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental
@ApiStatus.Internal
@Serializable
class SeCommandInfo (
val command: String,
val definition: String,
val providerId: String,
) {
constructor(searchEverywhereCommandInfo: SearchEverywhereCommandInfo, providerId: String) : this(
command = searchEverywhereCommandInfo.command,
definition = searchEverywhereCommandInfo.definition,
providerId = providerId
)
}
@@ -0,0 +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 com.intellij.platform.searchEverywhere
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface SeCommandsProviderInterface {
fun getSupportedCommands(): List<SeCommandInfo>
}
@@ -22,6 +22,8 @@ class SeItemData private constructor(
val additionalInfo: Map<String, String>,
private val itemRef: DurableRef<SeItemEntity>,
) {
val isCommand: Boolean get() = additionalInfo[SeItemDataKeys.IS_COMMAND]?.toBoolean() == true
fun fetchItemIfExists(): SeItem? {
return itemRef.derefOrNull()?.findItemOrNull()
}
@@ -8,4 +8,5 @@ object SeItemDataKeys {
const val IS_SEMANTIC: String = "SeItemDataKeys.IsSemantic"
const val PSI_LANGUAGE_ID: String = "SeItemDataKeys.PsiLanguage"
const val REPORTABLE_PROVIDER_ID: String = "SeItemDataKeys.ReportableProviderId"
const val IS_COMMAND: String = "SeItemDataKeys.IsCommand"
}
@@ -131,11 +131,21 @@ interface SeRemoteApi : RemoteApi<Unit> {
isAllTab: Boolean,
): Boolean
suspend fun isExtendedInfoEnabled(projectId: ProjectId,
session: SeSession,
dataContextId: DataContextId,
providerIds: List<SeProviderId>,
isAllTab: Boolean): Boolean
suspend fun isExtendedInfoEnabled(
projectId: ProjectId,
session: SeSession,
dataContextId: DataContextId,
providerIds: List<SeProviderId>,
isAllTab: Boolean,
): Boolean
suspend fun isCommandsSupported(
projectId: ProjectId,
session: SeSession,
dataContextId: DataContextId,
providerIds: List<SeProviderId>,
isAllTab: Boolean,
): Boolean
companion object {
@JvmStatic
@@ -60,6 +60,14 @@ class SeAdaptedItemsProvider(contributor: SearchEverywhereContributor<Any>) : Se
return contributorWrapper.contributor is SearchEverywhereExtendedInfoProvider
}
fun isCommandsSupported(): Boolean {
return contributorWrapper.contributor.supportedCommands.isNotEmpty()
}
fun getSupportedCommands(): List<SeCommandInfo> {
return contributorWrapper.contributor.supportedCommands.map { commandInfo -> SeCommandInfo(commandInfo, id) }
}
override fun dispose() {
Disposer.dispose(contributorWrapper)
}
@@ -1,6 +1,7 @@
// 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.providers
import com.intellij.ide.SearchTopHitProvider
import com.intellij.ide.actions.searcheverywhere.statistics.SearchEverywhereUsageTriggerCollector
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.EDT
@@ -8,8 +9,8 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.platform.scopes.SearchScopesInfo
import com.intellij.platform.searchEverywhere.*
import com.intellij.platform.searchEverywhere.providers.commands.SeCommandItem
import com.intellij.platform.searchEverywhere.providers.target.SeTypeVisibilityStatePresentation
import com.intellij.platform.searchEverywhere.providers.topHit.SeTopHitItemsProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
@@ -19,9 +20,12 @@ import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import java.util.*
import kotlin.collections.filter
import kotlin.collections.map
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.concurrent.atomics.incrementAndFetch
import kotlin.to
@ApiStatus.Internal
class SeLocalItemDataProvider(
@@ -44,19 +48,72 @@ class SeLocalItemDataProvider(
@OptIn(ExperimentalAtomicApi::class)
fun getItems(params: SeParams): Flow<SeItemData> {
val counter = AtomicInt(0)
return getRawItems(params).mapNotNull { item ->
val itemData = SeItemData.createItemData(session, UUID.randomUUID().toString(), item, id, item.weight(), item.presentation(), infoWithReportableId, emptyList())
itemData?.also {
SeLog.log(SeLog.ITEM_EMIT) {
val count = counter.incrementAndFetch()
"$logLabel provider for ${id.value} receives (total=$count, priority=${itemData.weight}): ${itemData.uuid} - ${itemData.presentation.text.split("\n").firstOrNull()}"
}
}
}.buffer(0, onBufferOverflow = BufferOverflow.SUSPEND)
val commandItemsFlow = createCommandItemsFlow(params, counter)
val rawItemsFlow = createRawItemsFlow(params, counter)
return merge(commandItemsFlow, rawItemsFlow)
.buffer(0, onBufferOverflow = BufferOverflow.SUSPEND)
}
@OptIn(ExperimentalAtomicApi::class)
private fun createCommandItemsFlow(
params: SeParams,
counter: AtomicInt,
): Flow<SeItemData> = channelFlow {
val supportedCommands = getSupportedCommands()
val commandItems = getCommandItems(params, supportedCommands)
for (item in commandItems) {
SeItemData.createItemData(session, UUID.randomUUID().toString(), item, id, item.weight(),
item.presentation(), mapOf(SeItemDataKeys.IS_COMMAND to "true"), emptyList()
)?.also { itemData ->
val count = counter.incrementAndFetch()
SeLog.log(SeLog.ITEM_EMIT) {
"$logLabel provider for ${id.value} receives command (total=$count): " +
"${itemData.presentation.text.split("\n").firstOrNull()}"
}
send(itemData)
}
}
}.buffer(0, onBufferOverflow = BufferOverflow.SUSPEND)
private fun getCommandItems(params: SeParams, supportedCommands: List<SeCommandInfo>): List<SeCommandItem> {
val inputQuery = params.inputQuery
if (!inputQuery.isCommandQuery) return emptyList()
val commandPrefix = SearchTopHitProvider.getTopHitAccelerator()
val typedCommand = inputQuery.removePrefix(commandPrefix)
val matchingCommands = supportedCommands
.filter { it.command.contains(typedCommand) }
SeLog.log(SeLog.ITEM_EMIT) {
"Command item list completed - $logLabel - ${matchingCommands.size}"
}
return matchingCommands.map { SeCommandItem(it) }
}
@OptIn(ExperimentalAtomicApi::class)
fun createRawItemsFlow(
params: SeParams,
counter: AtomicInt,
): Flow<SeItemData> = getRawItems(params).mapNotNull { item ->
val itemData = SeItemData.createItemData(session, UUID.randomUUID().toString(), item, id, item.weight(), item.presentation(), infoWithReportableId, emptyList())
itemData?.also {
SeLog.log(SeLog.ITEM_EMIT) {
val count = counter.incrementAndFetch()
"$logLabel provider for ${id.value} receives (total=$count, priority=${itemData.weight}): ${itemData.uuid} - ${itemData.presentation.text.split("\n").firstOrNull()}"
}
}
}.buffer(0, onBufferOverflow = BufferOverflow.SUSPEND)
@OptIn(ExperimentalAtomicApi::class)
fun getRawItems(params: SeParams): Flow<SeItem> {
if (params.inputQuery.isCommandWithArgs && !isCommandsSupported()) {
return emptyFlow()
}
return channelFlow {
try {
provider.collectItems(params) { item ->
@@ -110,26 +167,38 @@ class SeLocalItemDataProvider(
}
fun isPreviewEnabled(): Boolean {
if (provider is SeTopHitItemsProvider) {
return provider.isPreviewProvider()
} else if (provider is SeAdaptedItemsProvider) {
if (provider is SeAdaptedItemsProvider) {
return provider.isPreviewProvider()
}
return provider is SeItemsPreviewProvider
}
fun isExtendedInfoEnabled(): Boolean {
if (provider is SeTopHitItemsProvider) {
return provider.isExtendedInfoProvider()
}
else if (provider is SeAdaptedItemsProvider) {
if (provider is SeAdaptedItemsProvider) {
return provider.isExtendedInfoProvider()
}
return provider is SeExtendedInfoProvider
}
fun isCommandsSupported(): Boolean {
if (provider is SeAdaptedItemsProvider) {
return provider.isCommandsSupported()
}
return provider is SeCommandsProviderInterface
}
fun getSupportedCommands(): List<SeCommandInfo> {
if (provider is SeAdaptedItemsProvider) {
return provider.getSupportedCommands()
}
return (provider as? SeCommandsProviderInterface)?.getSupportedCommands() ?: emptyList()
}
override fun dispose() {
SeLog.log(SeLog.LIFE_CYCLE, "$logLabel provider ${id.value} disposed")
Disposer.dispose(provider)
}
}
}
private val String.isCommandQuery get() = startsWith(SearchTopHitProvider.getTopHitAccelerator()) && !contains(" ")
private val String.isCommandWithArgs get() = startsWith(SearchTopHitProvider.getTopHitAccelerator()) && contains(" ")
@@ -0,0 +1,28 @@
// 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.providers.commands
import com.intellij.ide.SearchTopHitProvider
import com.intellij.ide.ui.icons.rpcId
import com.intellij.platform.searchEverywhere.SeCommandInfo
import com.intellij.platform.searchEverywhere.SeItem
import com.intellij.platform.searchEverywhere.SeItemPresentation
import com.intellij.platform.searchEverywhere.SeTargetItemPresentation
import com.intellij.util.ui.EmptyIcon
import org.jetbrains.annotations.ApiStatus.Internal
@Internal
class SeCommandItem(
val commandInfo: SeCommandInfo,
) : SeItem {
override fun weight(): Int = 10 // same as in SearchEverywhereUI.myStubCommandContributor
override suspend fun presentation(): SeItemPresentation {
return SeTargetItemPresentation(
iconId = EmptyIcon.ICON_16.rpcId(),
presentableText = SearchTopHitProvider.getTopHitAccelerator() + commandInfo.command,
containerText = commandInfo.definition,
extendedInfo = null,
isMultiSelectionSupported = false)
}
override val rawObject: Any get() = commandInfo
}
@@ -2,8 +2,6 @@
package com.intellij.platform.searchEverywhere.providers.topHit
import com.intellij.ide.actions.searcheverywhere.SearchEverywhereContributor
import com.intellij.ide.actions.searcheverywhere.SearchEverywherePreviewProvider
import com.intellij.ide.actions.searcheverywhere.SearchEverywhereExtendedInfoProvider
import com.intellij.openapi.application.EDT
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
@@ -35,7 +33,7 @@ open class SeTopHitItemsProvider(
private val project: Project,
private val contributorWrapper: SeAsyncContributorWrapper<Any>,
override val displayName: @Nls String,
) : SeWrappedLegacyContributorItemsProvider() {
) : SeWrappedLegacyContributorItemsProvider(), SeCommandsProviderInterface {
override val contributor: SearchEverywhereContributor<Any> = contributorWrapper.contributor
override val id: String get() = id(isHost)
@@ -61,12 +59,8 @@ open class SeTopHitItemsProvider(
return contributor.showInFindResults()
}
fun isPreviewProvider(): Boolean {
return contributorWrapper.contributor is SearchEverywherePreviewProvider
}
fun isExtendedInfoProvider(): Boolean {
return contributor is SearchEverywhereExtendedInfoProvider
override fun getSupportedCommands(): List<SeCommandInfo> {
return contributor.supportedCommands.map { commandInfo -> SeCommandInfo(commandInfo, id) }
}
override fun dispose() {
@@ -60,6 +60,13 @@ class SeResultsCountBalancer(private val logLabel: String,
}
suspend fun add(newItem: SeItemData): SeItemData {
if (newItem.isCommand) {
SeLog.logSuspendable(SeLog.BALANCING) {
"($logLabel) Command item ${newItem.presentation.text} with provider ${newItem.providerId} is returned without balancing"
}
return newItem
}
allProvidersCounts[newItem.providerId]?.fetchAndIncrement()
highPriorityPermits[newItem.providerId]?.acquire()
@@ -62,6 +62,10 @@ class SeGitTab(private val delegate: SeTabDelegate) : SeTab {
return delegate.isExtendedInfoEnabled()
}
override suspend fun isCommandsSupported(): Boolean {
return delegate.isCommandsSupported()
}
override fun dispose() {
Disposer.dispose(delegate)
}