mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[platform UI] IJPL-206730 Search Everywhere for RD: Support commands via "/"
GitOrigin-RevId: fc01068a606b2841cf0a5e4ea97cbf15b24b7e38
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4c04714771
commit
1dee6dc99c
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -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()
|
||||
|
||||
+4
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user