diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/AbstractGotoSEContributor.kt b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/AbstractGotoSEContributor.kt index de2cd5abb6e1..bf4a1d6087c5 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/AbstractGotoSEContributor.kt +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/AbstractGotoSEContributor.kt @@ -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? + @ApiStatus.Internal var contributorModules: List? = null + + @ApiStatus.Internal + protected constructor(event: AnActionEvent, contributorModules: List?) : 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 doGetActions( filter: PersistentSearchEverywhereContributorFilter?, statisticsCollector: ElementsChooser.StatisticsCollector?, @@ -214,8 +227,7 @@ abstract class AbstractGotoSEContributor protected constructor(event: AnActionEv return emptyList() } - val result = ArrayList() - 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() + 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>, + ) { + fetchWeightedElementsMixing( + pattern, progressIndicator, consumer, + { localPattern, localProgressIndicator, localConsumer -> performByGotoContributorSearch(localPattern, localProgressIndicator, localConsumer) }, + *( + contributorModules?.map2Array>) -> Unit> { + { localPattern, localProgressIndicator, localConsumer -> it.perProductFetchWeightedElements(localPattern, localProgressIndicator, localConsumer) } + } ?: emptyArray() + ) + ) + } + + private fun performByGotoContributorSearch( + pattern: String, + progressIndicator: ProgressIndicator, + consumer: Processor> ) { 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().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().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().navigate(it, navigationOptions) - } - } - } - else { - project.serviceAsync().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().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().navigate(it, navigationOptions) + } + } + } + else { + project.serviceAsync().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 = SearchEverywherePsiRenderer(this) + override fun getElementsRenderer(): ListCellRenderer = + contributorModules?.firstNotNullOfOrNull { it.getOverridingElementRenderer(this) } + ?: SearchEverywherePsiRenderer(this) @Suppress("OVERRIDE_DEPRECATION") override fun getElementPriority(element: Any, searchPattern: String): Int = 50 diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/ClassSearchEverywhereContributor.kt b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/ClassSearchEverywhereContributor.kt index fed6189294d1..990e167dc6a6 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/ClassSearchEverywhereContributor.kt +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/ClassSearchEverywhereContributor.kt @@ -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?) : this(event) { + this.contributorModules = contributorModules + } + companion object { @JvmStatic fun createLanguageFilter(project: Project): PersistentSearchEverywhereContributorFilter { @@ -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) diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereContributorModule.kt b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereContributorModule.kt new file mode 100644 index 000000000000..992cc880198f --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereContributorModule.kt @@ -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 + + fun perProductFetchWeightedElements(pattern: String, progressIndicator: ProgressIndicator, consumer: Processor>) + + fun currentSearchEverywhereToggledActionChanged(newAction: SearchEverywhereToggleAction) +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SymbolSearchEverywhereContributor.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SymbolSearchEverywhereContributor.java index 4aa9a645e0a9..bbcac217ed77 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SymbolSearchEverywhereContributor.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SymbolSearchEverywhereContributor.java @@ -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 myFilter; + @ApiStatus.Internal + public SymbolSearchEverywhereContributor(@NotNull AnActionEvent event, @Nullable List 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)); diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/Utils.kt b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/Utils.kt new file mode 100644 index 000000000000..c76cdb4f5780 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/Utils.kt @@ -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>, + vararg fetchers: (String, ProgressIndicator, Processor>) -> Unit, +) { + val itemPool = mutableListOf>() + val adderClosure = { it: FoundItemDescriptor -> 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 makeTypeErasingConsumer(processor: Processor>) = + { genericDescriptor: FoundItemDescriptor -> (genericDescriptor as? FoundItemDescriptor)?.let { processor.process(it) } == true } \ No newline at end of file