[Rider] Composable SE contributor extensions, part 1

Now AbstractSEContributor is extensible by a list of modules, which are essentially "sub-contributors".

(cherry picked from commit c0ac76f0740863689e4cb70e64b9efbc25a0b6b6)

GitOrigin-RevId: 675858bd0d19a6895a71146ea3e71b208a808ba1
This commit is contained in:
Egor.Skrypnikov
2025-03-18 17:18:35 +03:00
committed by intellij-monorepo-bot
parent 0ede7170c0
commit 78918f00c0
5 changed files with 217 additions and 61 deletions

View File

@@ -51,6 +51,8 @@ import com.intellij.psi.search.SearchScope
import com.intellij.psi.util.PsiUtilCore
import com.intellij.util.IntPair
import com.intellij.util.Processor
import com.intellij.util.containers.map2Array
import com.intellij.util.containers.toArray
import com.intellij.util.indexing.FindSymbolParameters
import it.unimi.dsi.fastutil.ints.IntArrayList
import kotlinx.coroutines.CoroutineScope
@@ -89,6 +91,13 @@ abstract class AbstractGotoSEContributor protected constructor(event: AnActionEv
@JvmField
protected val myPsiContext: SmartPsiElementPointer<PsiElement?>?
@ApiStatus.Internal var contributorModules: List<SearchEverywhereContributorModule>? = null
@ApiStatus.Internal
protected constructor(event: AnActionEvent, contributorModules: List<SearchEverywhereContributorModule>?) : this(event) {
this.contributorModules = contributorModules
}
init {
val context = GotoActionBase.getPsiContext(event)
myPsiContext = if (context == null) null else SmartPointerManager.getInstance(myProject).createSmartPsiElementPointer(context)
@@ -205,6 +214,10 @@ abstract class AbstractGotoSEContributor protected constructor(event: AnActionEv
override fun isShownInSeparateTab(): Boolean = true
@ApiStatus.Internal
protected var currentSearchEverywhereAction: SearchEverywhereToggleAction? = null
private set
protected fun <T> doGetActions(
filter: PersistentSearchEverywhereContributorFilter<T>?,
statisticsCollector: ElementsChooser.StatisticsCollector<T>?,
@@ -214,8 +227,7 @@ abstract class AbstractGotoSEContributor protected constructor(event: AnActionEv
return emptyList()
}
val result = ArrayList<AnAction>()
result.add(object : ScopeChooserAction() {
val toggleAction = object : ScopeChooserAction() {
val canToggleEverywhere = everywhereScope != projectScope
override fun onScopeSelected(o: ScopeDescriptor) {
@@ -249,7 +261,12 @@ abstract class AbstractGotoSEContributor protected constructor(event: AnActionEv
override fun setScopeIsDefaultAndAutoSet(scopeDefaultAndAutoSet: Boolean) {
isScopeDefaultAndAutoSet = scopeDefaultAndAutoSet
}
})
}
currentSearchEverywhereAction = toggleAction
val result = ArrayList<AnAction>()
result.add(toggleAction)
result.add(PreviewAction())
result.add(SearchEverywhereFiltersAction(filter, onChanged, statisticsCollector))
return result
@@ -277,6 +294,22 @@ abstract class AbstractGotoSEContributor protected constructor(event: AnActionEv
pattern: String,
progressIndicator: ProgressIndicator,
consumer: Processor<in FoundItemDescriptor<Any>>,
) {
fetchWeightedElementsMixing(
pattern, progressIndicator, consumer,
{ localPattern, localProgressIndicator, localConsumer -> performByGotoContributorSearch(localPattern, localProgressIndicator, localConsumer) },
*(
contributorModules?.map2Array<SearchEverywhereContributorModule, (String, ProgressIndicator, Processor<in FoundItemDescriptor<Any>>) -> Unit> {
{ localPattern, localProgressIndicator, localConsumer -> it.perProductFetchWeightedElements(localPattern, localProgressIndicator, localConsumer) }
} ?: emptyArray()
)
)
}
private fun performByGotoContributorSearch(
pattern: String,
progressIndicator: ProgressIndicator,
consumer: Processor<in FoundItemDescriptor<Any>>
) {
if (!isEmptyPatternSupported && pattern.isEmpty()) {
return
@@ -298,6 +331,17 @@ abstract class AbstractGotoSEContributor protected constructor(event: AnActionEv
val everywhere = scope.isSearchInLibraries
val viewModel = MyViewModel(myProject, model)
if (LOG.isTraceEnabled) {
LOG.trace(buildString {
append("!! Collecting Goto SE items for ").append(this@AbstractGotoSEContributor::class.simpleName).append(" !!\n")
append("PSI context is: ").append(context).append("\n")
append("Provider is: ").append(provider::class.simpleName).append("\n")
append("Search scope is: ").append(scope.displayName).append("\n")
append("Is libraries search? ").append(if (everywhere) "YES" else "NO").append("\n")
})
}
when (provider) {
is ChooseByNameInScopeItemProvider -> {
val parameters = FindSymbolParameters.wrap(pattern, scope)
@@ -378,75 +422,98 @@ abstract class AbstractGotoSEContributor protected constructor(event: AnActionEv
override fun showInFindResults(): Boolean = true
// This sets off-the-stack coroutining for some (most) inputs. Take care.
// a "true" return value does NOT mean that the navigation was successful.
override fun processSelectedItem(selected: Any, modifiers: Int, searchText: String): Boolean {
contributorModules?.forEach {
val processedFlag = it.processSelectedItem(selected, modifiers, searchText)
if (processedFlag != null) {
return processedFlag
}
}
return processByGotoSelectedItem(selected, modifiers, searchText)
}
@Suppress("SameReturnValue")
private fun processByGotoSelectedItem(selected: Any, modifiers: Int, searchText: String): Boolean {
if (selected !is PsiElement) {
if (LOG.isTraceEnabled) {
LOG.trace("Selected item for $searchText is not PsiElement, it is: ${selected}; performing non-suspending navigation")
}
EditSourceUtil.navigate((selected as NavigationItem), true, false)
return true
}
project.service<SearchEverywhereContributorCoroutineScopeHolder>().coroutineScope.launch(ClientId.coroutineContext()) {
val command = readAction {
if (!selected.isValid) {
LOG.warn("Cannot navigate to invalid PsiElement")
return@readAction null
}
val psiElement = preparePsi(selected, searchText)
val file =
if (selected is PsiFile) selected.virtualFile
else PsiUtilCore.getVirtualFile(psiElement)
val extendedNavigatable = if (file == null) {
null
}
else {
val position = getLineAndColumn(searchText)
if (position.first >= 0 || position.second >= 0) {
//todo create navigation request by line&column, not by offset only
OpenFileDescriptor(project, file, position.first, position.second)
}
else {
null
}
}
suspend {
@Suppress("DEPRECATION")
val navigationOptions = NavigationOptions.defaultOptions()
.openInRightSplit((modifiers and InputEvent.SHIFT_MASK) != 0)
.preserveCaret(true)
if (extendedNavigatable == null) {
if (file == null) {
val navigatable = psiElement as? Navigatable
if (navigatable != null) {
// Navigation items from rd protocol often lack .containingFile or other PSI extensions, and are only expected to be
// navigated through the Navigatable API.
// This fallback is for items like that.
val navRequest = RawNavigationRequest(navigatable, true)
project.serviceAsync<NavigationService>().navigate(navRequest, navigationOptions)
} else {
LOG.warn("Cannot navigate to invalid PsiElement (psiElement=$psiElement, selected=$selected)")
}
}
else {
createSourceNavigationRequest(element = psiElement, file = file, searchText = searchText)?.let {
project.serviceAsync<NavigationService>().navigate(it, navigationOptions)
}
}
}
else {
project.serviceAsync<NavigationService>().navigate(extendedNavigatable, navigationOptions)
triggerLineOrColumnFeatureUsed(extendedNavigatable)
}
}
val navigatingAction = readAction { tryMakeNavigatingFunction(selected, modifiers, searchText) }
if (navigatingAction != null) {
navigatingAction()
}
else {
LOG.warn("Selected $selected produced an invalid navigation action! Doing nothing!")
}
command?.invoke()
}
return true
}
private fun tryMakeNavigatingFunction(selected: PsiElement, modifiers: Int, searchText: String): (suspend () -> Unit)? {
if (!selected.isValid) {
LOG.warn("Cannot navigate to invalid PsiElement")
return null
}
val psiElement = preparePsi(selected, searchText)
val file =
if (selected is PsiFile) selected.virtualFile
else PsiUtilCore.getVirtualFile(psiElement)
val extendedNavigatable = if (file == null) {
null
}
else {
val position = getLineAndColumn(searchText)
if (position.first >= 0 || position.second >= 0) {
//todo create navigation request by line&column, not by offset only
OpenFileDescriptor(project, file, position.first, position.second)
}
else {
null
}
}
return suspend {
@Suppress("DEPRECATION")
val navigationOptions = NavigationOptions.defaultOptions()
.openInRightSplit((modifiers and InputEvent.SHIFT_MASK) != 0)
.preserveCaret(true)
if (extendedNavigatable == null) {
if (file == null) {
val navigatable = psiElement as? Navigatable
if (navigatable != null) {
// Navigation items from rd protocol often lack .containingFile or other PSI extensions, and are only expected to be
// navigated through the Navigatable API.
// This fallback is for items like that.
val navRequest = RawNavigationRequest(navigatable, true)
project.serviceAsync<NavigationService>().navigate(navRequest, navigationOptions)
} else {
LOG.warn("Cannot navigate to invalid PsiElement (psiElement=$psiElement, selected=$selected)")
}
}
else {
createSourceNavigationRequest(element = psiElement, file = file, searchText = searchText)?.let {
project.serviceAsync<NavigationService>().navigate(it, navigationOptions)
}
}
}
else {
project.serviceAsync<NavigationService>().navigate(extendedNavigatable, navigationOptions)
triggerLineOrColumnFeatureUsed(extendedNavigatable)
}
}
}
@ApiStatus.Internal
protected open suspend fun createSourceNavigationRequest(
element: PsiElement,
@@ -488,7 +555,9 @@ abstract class AbstractGotoSEContributor protected constructor(event: AnActionEv
override fun isDumbAware(): Boolean = isDumbAware(createModel(myProject))
override fun getElementsRenderer(): ListCellRenderer<in Any?> = SearchEverywherePsiRenderer(this)
override fun getElementsRenderer(): ListCellRenderer<in Any?> =
contributorModules?.firstNotNullOfOrNull { it.getOverridingElementRenderer(this) }
?: SearchEverywherePsiRenderer(this)
@Suppress("OVERRIDE_DEPRECATION")
override fun getElementPriority(element: Any, searchPattern: String): Int = 50

View File

@@ -32,6 +32,11 @@ open class ClassSearchEverywhereContributor(event: AnActionEvent)
: AbstractGotoSEContributor(event), EssentialContributor, SearchEverywherePreviewProvider {
private val filter = createLanguageFilter(event.getRequiredData(CommonDataKeys.PROJECT))
@Internal
constructor(event: AnActionEvent, contributorModules: List<SearchEverywhereContributorModule>?) : this(event) {
this.contributorModules = contributorModules
}
companion object {
@JvmStatic
fun createLanguageFilter(project: Project): PersistentSearchEverywhereContributorFilter<LanguageRef> {
@@ -76,7 +81,9 @@ open class ClassSearchEverywhereContributor(event: AnActionEvent)
return super.getElementPriority(element, searchPattern) + 5
}
override fun createExtendedInfo(): ExtendedInfo? = createPsiExtendedInfo()
override fun createExtendedInfo(): ExtendedInfo? = createPsiExtendedInfo().let {
contributorModules?.firstNotNullOfOrNull { mod -> mod.mixinExtendedInfo(it) } ?: it
}
override suspend fun createSourceNavigationRequest(element: PsiElement, file: VirtualFile, searchText: String): NavigationRequest? {
val memberName = getMemberName(searchText)

View File

@@ -0,0 +1,27 @@
// 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.ide.actions.searcheverywhere
import com.intellij.openapi.Disposable
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.util.Processor
import org.jetbrains.annotations.ApiStatus
import javax.swing.ListCellRenderer
// Rider SE needs (a) deeper (b) more dynamic integrations with the search everywhere process
// due to
// (a) Rider essentially having its own Global Navigation implementation on its backend with its own rules.
// (b) Rider needing to -compositionally- integrate both with the "vanilla" and the "semantic" search.
@ApiStatus.Internal
interface SearchEverywhereContributorModule {
// Extended info should be handled differently. Prefer composition over tag interfaces and inheritance of ExtendedInfoProvider
fun mixinExtendedInfo(baseExtendedInfo: ExtendedInfo): ExtendedInfo
fun processSelectedItem(item: Any, modifiers: Int, searchTest: String): Boolean?
fun getOverridingElementRenderer(parent: Disposable): ListCellRenderer<in Any?>
fun perProductFetchWeightedElements(pattern: String, progressIndicator: ProgressIndicator, consumer: Processor<in FoundItemDescriptor<Any>>)
fun currentSearchEverywhereToggledActionChanged(newAction: SearchEverywhereToggleAction)
}

View File

@@ -9,6 +9,7 @@ import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -25,6 +26,12 @@ public class SymbolSearchEverywhereContributor extends AbstractGotoSEContributor
private final PersistentSearchEverywhereContributorFilter<LanguageRef> myFilter;
@ApiStatus.Internal
public SymbolSearchEverywhereContributor(@NotNull AnActionEvent event, @Nullable List<SearchEverywhereContributorModule> contributorModules) {
super(event, contributorModules);
myFilter = ClassSearchEverywhereContributor.createLanguageFilter(event.getRequiredData(CommonDataKeys.PROJECT));
}
public SymbolSearchEverywhereContributor(@NotNull AnActionEvent event) {
super(event);
myFilter = ClassSearchEverywhereContributor.createLanguageFilter(event.getRequiredData(CommonDataKeys.PROJECT));

View File

@@ -0,0 +1,46 @@
// 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.ide.actions.searcheverywhere
import com.intellij.openapi.application.readAction
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.indicatorRunBlockingCancellable
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.util.Processor
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import org.jetbrains.annotations.ApiStatus
/**
* A function that sequentially flat maps several Search Everywhere item fetch processes.
*
* A typical usage can be seen in [RiderGotoClassSearchEverywhereContributor] and [RiderGotoSymbolSearchEverywhereContributor].
* There it is used to fetch items both from the IDEA contributor and the Rider backend.
*
* @param consumeInCancellableRead switches whether the [finalConsumer] is applied under a [runBlockingCancellable] + [readAction] or not.
* Important for [PSIPresentationBgRendererWrapper], which implicitly requires [finalConsumer] to
* be run in such a context.
*/
@ApiStatus.Internal
@RequiresBackgroundThread
fun fetchWeightedElementsMixing(
pattern: String,
progressIndicator: ProgressIndicator,
finalConsumer: Processor<in FoundItemDescriptor<Any>>,
vararg fetchers: (String, ProgressIndicator, Processor<in FoundItemDescriptor<Any>>) -> Unit,
) {
val itemPool = mutableListOf<FoundItemDescriptor<Any>>()
val adderClosure = { it: FoundItemDescriptor<Any> -> itemPool.add(it) }
fetchers.forEach { fetcher -> fetcher(pattern, progressIndicator, adderClosure) }
itemPool.sortBy { -it.weight }
@Suppress("DEPRECATION")
indicatorRunBlockingCancellable(progressIndicator) {
readAction { itemPool.forEach { if (!finalConsumer.process(it)) return@readAction } }
}
}
@ApiStatus.Internal
@Suppress("UNCHECKED_CAST")
fun <T> makeTypeErasingConsumer(processor: Processor<in FoundItemDescriptor<Any>>) =
{ genericDescriptor: FoundItemDescriptor<T> -> (genericDescriptor as? FoundItemDescriptor<Any>)?.let { processor.process(it) } == true }