Revert "IJPL-188268 Search Everywhere: result selection gets lost on the popup reopening"

This reverts commit ab2e3edc90eb1148adeba6320a1a878467fbc925.

GitOrigin-RevId: 347ce0ebb8b3e916c3c33b415136b3c5a7d2222d
This commit is contained in:
Aydar Mukhametzyanov
2025-09-30 15:13:01 +00:00
committed by intellij-monorepo-bot
parent 5f8e291f29
commit c2acfa0868
7 changed files with 25 additions and 252 deletions
@@ -52,7 +52,6 @@ import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.SwingUtilities
import kotlin.String
@ApiStatus.Internal
@Service(Service.Level.PROJECT, Service.Level.APP)
@@ -75,8 +74,6 @@ class SeFrontendService(val project: Project?, private val coroutineScope: Corou
private val historyList = SearchHistoryList(true)
private var selectionState: SeSelectionState? = null
val removeSessionRef: AtomicBoolean = AtomicBoolean(true)
override fun show(tabId: String, searchText: String?, initEvent: AnActionEvent) {
@@ -183,8 +180,7 @@ class SeFrontendService(val project: Project?, private val coroutineScope: Corou
}
},
searchStatePublisher,
getStateService().getSize(POPUP_LOCATION_SETTINGS_KEY),
selectionState) {
getStateService().getSize(POPUP_LOCATION_SETTINGS_KEY)) {
popupScope.launch(NonCancellable) {
removeSessionRef.set(false)
try {
@@ -202,7 +198,6 @@ class SeFrontendService(val project: Project?, private val coroutineScope: Corou
popup = createPopup(contentPane, popupVm, project) {
completable.complete(Unit)
selectionState = contentPane.getSelectionState()
}
popup?.let { popup ->
@@ -1,71 +0,0 @@
// 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 com.intellij.platform.searchEverywhere.SeItemData
import com.intellij.platform.searchEverywhere.frontend.ui.SeResultJBList
import com.intellij.platform.searchEverywhere.frontend.ui.SeResultListItemRow
import com.intellij.platform.searchEverywhere.frontend.ui.SeResultListModel
import com.intellij.platform.searchEverywhere.frontend.ui.SeResultListRow
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class SeSelectionListener(
initialSelectionState: SeSelectionState?,
val resultList: SeResultJBList<SeResultListRow>,
val resultListModel: SeResultListModel,
) {
private var selectionState: SeSelectionState? = initialSelectionState
fun getIndexToSelect(maxVisibleRowCount: Int, currentPattern: String, isInitialSearchPattern: Boolean, isEndEvent: Boolean): Int {
if (selectionState == null) return 0
val selectedItemData = selectionState!!.selectedItem
if (resultListModel.size >= 1 && (resultListModel.get(0) as? SeResultListItemRow)?.item?.contentEquals(selectedItemData) == true) {
return 0
}
val effectiveModelSize = resultList.getEffectiveModelSize()
if (resultList.selectedIndex >= 1 || effectiveModelSize == 0) {
return -1
}
if (isEndEvent || (isInitialSearchPattern && selectionState!!.pattern != currentPattern)) {
selectionState = null
return 0
}
else {
val searchRange = 0..minOf(effectiveModelSize - 1, maxVisibleRowCount - 1)
val matchingIndex = searchRange.firstOrNull { i ->
(resultListModel.get(i) as? SeResultListItemRow)?.item?.contentEquals(selectedItemData) == true
}
if (matchingIndex != null) {
return matchingIndex
}
if (searchRange.last >= maxVisibleRowCount - 1) {
selectionState = null
}
return 0
}
}
fun saveSelectionState(currentPattern: String) {
if (resultList.selectedIndex == -1) {
return
}
val itemDataToSave = (resultListModel.get(resultList.selectedIndex) as? SeResultListItemRow)?.item
if (itemDataToSave != null) {
selectionState = SeSelectionState(currentPattern, itemDataToSave)
}
}
fun getSelectionState(): SeSelectionState? {
return selectionState
}
}
@ApiStatus.Internal
class SeSelectionState(val pattern: String, val selectedItem: SeItemData)
@@ -27,8 +27,6 @@ import com.intellij.platform.searchEverywhere.*
import com.intellij.platform.searchEverywhere.data.SeDataKeys
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.tabs.actions.SeActionItemPresentationRenderer
import com.intellij.platform.searchEverywhere.frontend.tabs.all.SeAllTab
import com.intellij.platform.searchEverywhere.frontend.tabs.files.SeTargetItemPresentationRenderer
@@ -65,7 +63,6 @@ import javax.swing.event.ListSelectionEvent
import javax.swing.text.Document
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.math.ceil
import kotlin.math.roundToInt
@OptIn(ExperimentalAtomicApi::class, ExperimentalCoroutinesApi::class)
@@ -74,7 +71,6 @@ class SePopupContentPane(private val project: Project?, private val vm: SePopupV
private val resizePopupHandler: (Dimension) -> Unit,
private val searchStatePublisher: SeSearchStatePublisher,
initPopupExtendedSize: Dimension?,
initialSelectionState: SeSelectionState?,
onShowFindToolWindow: () -> Unit) : JPanel(), Disposable, UiDataProvider {
val preferableFocusedComponent: JComponent get() = textField
val searchFieldDocument: Document get() = textField.document
@@ -88,21 +84,6 @@ class SePopupContentPane(private val project: Project?, private val vm: SePopupV
) { updatePopupWidthIfNecessary() }
private val textField = object : SeTextField() {
private var _isInitialSearchPattern: Boolean = true
val isInitialSearchPattern: Boolean get() = _isInitialSearchPattern
init {
text = vm.searchPattern.value
selectAll()
document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: javax.swing.event.DocumentEvent) {
vm.setSearchText(text)
_isInitialSearchPattern = false
}
})
}
override fun getAccessibleContext(): AccessibleContext {
if (accessibleContext == null) {
accessibleContext = TextFieldWithListAccessibleContext(this, resultList.getAccessibleContext())
@@ -114,8 +95,7 @@ class SePopupContentPane(private val project: Project?, private val vm: SePopupV
private val minWidth = Registry.intValue("search.everywhere.new.minimum.width", 700)
private val resultListModel = SeResultListModel(searchStatePublisher) { resultList.selectionModel }
private val resultList: SeResultJBList<SeResultListRow> = SeResultJBList(resultListModel)
private var selectionListener = SeSelectionListener(initialSelectionState, resultList, resultListModel)
private val resultList: JBList<SeResultListRow> = JBList(resultListModel)
private val resultsScrollPane = createListPane(resultList)
private val extendedInfoContainer: JComponent = JPanel(BorderLayout())
@@ -172,6 +152,14 @@ class SePopupContentPane(private val project: Project?, private val vm: SePopupV
.row(resizable = true).cell(resultsScrollPane, horizontalAlign = HorizontalAlign.FILL, verticalAlign = VerticalAlign.FILL, resizableColumn = true)
.row().cell(extendedInfoContainer, horizontalAlign = HorizontalAlign.FILL, resizableColumn = true)
textField.text = vm.searchPattern.value
textField.selectAll()
textField.document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: javax.swing.event.DocumentEvent) {
vm.setSearchText(textField.text)
}
})
if (textField.text.isNotEmpty()) {
isCompactViewMode = false
}
@@ -240,7 +228,6 @@ class SePopupContentPane(private val project: Project?, private val vm: SePopupV
updateEmptyStatus()
}
autoSelectIndex(searchContext.searchPattern, true)
updateViewMode()
}
}.collect { event ->
@@ -254,7 +241,11 @@ class SePopupContentPane(private val project: Project?, private val vm: SePopupV
if (wasFrozen) resultListModel.freezer.enable()
updateFrozenCount()
autoSelectIndex(searchContext.searchPattern, false)
// Autoselect the first element if there were no selection preserved during the update
if (resultListModel.size > 0 && resultList.selectedIndices.isEmpty()) {
resultList.selectedIndex = 0
}
updateViewMode()
}
}
@@ -497,12 +488,6 @@ class SePopupContentPane(private val project: Project?, private val vm: SePopupV
extendedInfoComponent?.updateElement(resultList.selectedValue, this@SePopupContentPane)
}
}
resultList.addListSelectionListener { _: ListSelectionEvent ->
if (!resultList.isAutoSelectionChange) {
selectionListener.saveSelectionState(textField.text)
}
}
}
private fun initSearchActions() {
@@ -583,9 +568,11 @@ class SePopupContentPane(private val project: Project?, private val vm: SePopupV
private fun scrollList(down: Boolean) {
if (resultList.model.size == 0) return
if (down) {
val visibleRowCount = getMaxVisibleRowCount()
val cellHeight = resultList.getCellBounds(0, 0)?.height ?: return
val viewportHeight = resultsScrollPane.viewport.height
val visibleRowCount = viewportHeight / cellHeight
val shiftSize = maxOf(1, visibleRowCount - 4)
val shiftSize = maxOf(1, visibleRowCount - 3)
val targetIndex = resultList.selectedIndex + shiftSize
val modelSize = resultList.model.size
val hasMoreRow = modelSize > 0 && resultList.model.getElementAt(modelSize - 1) is SeResultListMoreRow
@@ -816,7 +803,7 @@ class SePopupContentPane(private val project: Project?, private val vm: SePopupV
headerPane.preferredSize.height + textField.preferredSize.height
}
else {
getPopupExtendedHeight()
popupExtendedSize?.height ?: JBUI.CurrentTheme.BigPopup.maxListHeight()
}
val preferredWidth = popupExtendedSize?.width ?: maxOf(resultsScrollPane.preferredSize.width,
@@ -825,10 +812,6 @@ class SePopupContentPane(private val project: Project?, private val vm: SePopupV
return Dimension(preferredWidth, preferredHeight)
}
private fun getPopupExtendedHeight(): Int {
return popupExtendedSize?.height ?: JBUI.CurrentTheme.BigPopup.maxListHeight()
}
private fun logTabSwitchedEvent(e: AnActionEvent) {
SearchEverywhereUsageTriggerCollector.TAB_SWITCHED.log(project,
SearchEverywhereUsageTriggerCollector.CONTRIBUTOR_ID_FIELD.with(vm.currentTab.tabId),
@@ -874,35 +857,11 @@ class SePopupContentPane(private val project: Project?, private val vm: SePopupV
}
}
/**
* Calculates the number of rows that can be visible in the scroll pane, including partially visible rows.
*
* @return the total count of visible rows (both fully and partially visible), or -1 if cell height cannot be determined
*/
private fun getMaxVisibleRowCount(): Int {
val cellHeight = resultList.getCellBounds(0, 0)?.height ?: -1
val scrollPaneHeight = getPopupExtendedHeight() - headerPane.height - textField.height - (extendedInfoComponent?.component?.height ?: 0)
return ceil(scrollPaneHeight.toDouble() / cellHeight).toInt()
}
@TestOnly
fun getResultListModel(): SeResultListModel {
return resultListModel
}
private fun autoSelectIndex(searchPattern: String, isEndEvent: Boolean) {
val indexToSelect = selectionListener.getIndexToSelect(getMaxVisibleRowCount(), searchPattern, textField.isInitialSearchPattern, isEndEvent)
if (indexToSelect != -1 && indexToSelect < resultListModel.size()) {
resultList.autoSelectIndex(indexToSelect)
ScrollingUtil.ensureIndexIsVisible(resultList, resultList.selectedIndex, 1)
}
}
fun getSelectionState() : SeSelectionState? {
return selectionListener.getSelectionState()
}
override fun dispose() {}
companion object {
@@ -1,38 +0,0 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.searchEverywhere.frontend.ui
import com.intellij.ui.components.JBList
import com.intellij.util.concurrency.annotations.RequiresEdt
import org.jetbrains.annotations.ApiStatus
import javax.swing.ListModel
@ApiStatus.Internal
class SeResultJBList<E : SeResultListRow>(model: ListModel<E>) : JBList<E>(model) {
private var _isAutoSelectionChange: Boolean = false
val isAutoSelectionChange: Boolean
get() = _isAutoSelectionChange
/**
* Returns the number of items in the result list, excluding the loading indicator.
*/
fun getEffectiveModelSize(): Int {
val modelSize = model.size
if (modelSize == 0) return 0
// Check if the last element is SeResultListMoreRow (loading indicator)
val lastElement = model.getElementAt(modelSize - 1)
return if (lastElement is SeResultListMoreRow) {
modelSize - 1
} else {
modelSize
}
}
@RequiresEdt
fun autoSelectIndex(index: Int) {
_isAutoSelectionChange = true
selectedIndex = index
_isAutoSelectionChange = false
}
}
@@ -13,7 +13,6 @@
- a:weight():I
*:com.intellij.platform.searchEverywhere.SeItemPresentation
- *sf:Companion:com.intellij.platform.searchEverywhere.SeItemPresentation$Companion
- contentEquals(com.intellij.platform.searchEverywhere.SeItemPresentation):Z
- getExtendedInfo():com.intellij.platform.searchEverywhere.SeExtendedInfo
- a:getText():java.lang.String
- a:isMultiSelectionSupported():Z
@@ -50,7 +50,7 @@ class SeItemData private constructor(
val additionalInfo = additionalInfo.toMutableMap()
if (item is SeLegacyItem) {
computeCatchingOrNull(true, { e -> "Couldn't add language info (${providerId.value}): $e" }) {
computeCatchingOrNull(true, { e-> "Couldn't add language info (${providerId.value}): $e" }) {
PSIPresentationBgRendererWrapper.toPsi(item.rawObject)?.let {
readAction {
additionalInfo[SeItemDataKeys.PSI_LANGUAGE_ID] = it.language.id
@@ -58,7 +58,7 @@ class SeItemData private constructor(
}
}
computeCatchingOrNull(true, { e -> "Couldn't add isSemantic info (${providerId.value}): $e" }) {
computeCatchingOrNull(true, { e-> "Couldn't add isSemantic info (${providerId.value}): $e" }) {
val isSemanticElement = (item.contributor as? SemanticSearchEverywhereContributor)?.isElementSemantic(item.rawObject) ?: false
additionalInfo[SeItemDataKeys.IS_SEMANTIC] = isSemanticElement.toString()
}
@@ -67,11 +67,4 @@ class SeItemData private constructor(
return SeItemData(uuid, providerId, weight, presentation, uuidToReplace, additionalInfo, entityRef)
}
}
fun contentEquals(other: Any?): Boolean {
if (this === other) return true
if (other !is SeItemData) return false
return presentation.contentEquals(other.presentation)
}
}
@@ -29,11 +29,6 @@ sealed interface SeItemPresentation {
val text: String
val extendedInfo: SeExtendedInfo? get() = null
val isMultiSelectionSupported: Boolean
fun contentEquals(other: SeItemPresentation?): Boolean {
if (other == null) return false
return text == other.text && extendedInfo?.actionText == other.extendedInfo?.actionText
}
}
@ApiStatus.Internal
@@ -64,16 +59,6 @@ class SeSimpleItemPresentation(
accessibleAdditionToText,
extendedInfo,
isMultiSelectionSupported)
override fun contentEquals(other: SeItemPresentation?): Boolean {
if (this === other) return true
if (other !is SeSimpleItemPresentation) return false
return super.contentEquals(other) &&
selectedTextChunk?.text == other.selectedTextChunk?.text &&
description == other.description &&
accessibleAdditionToText == other.accessibleAdditionToText
}
}
@ApiStatus.Internal
@@ -93,13 +78,6 @@ sealed interface SeActionItemPresentation : SeItemPresentation {
_switcherState = _switcherState?.not()
}
}
override fun contentEquals(other: SeItemPresentation?): Boolean {
if (this === other) return true
if (other !is SeActionItemPresentation) return false
return super.contentEquals(other) && commonData == other.commonData
}
}
@ApiStatus.Internal
@@ -124,18 +102,6 @@ data class SeRunnableActionItemPresentation(
val productIconId: IconId?,
val callToActionText: @Nls String,
)
override fun contentEquals(other: SeItemPresentation?): Boolean {
if (this === other) return true
if (other !is SeRunnableActionItemPresentation) return false
return super.contentEquals(other) &&
commonData == other.commonData &&
toolTip == other.toolTip &&
actionId == other.actionId &&
shortcut == other.shortcut &&
promo == other.promo
}
}
@ApiStatus.Internal
@@ -148,16 +114,6 @@ data class SeOptionActionItemPresentation(
) : SeActionItemPresentation {
override val text: String get() = commonData.text
override val extendedInfo: SeExtendedInfo? get() = commonData.extendedInfo
override fun contentEquals(other: SeItemPresentation?): Boolean {
if (this === other) return true
if (other !is SeOptionActionItemPresentation) return false
return super.contentEquals(other) &&
commonData == other.commonData &&
value == other.value &&
isBooleanOption == other.isBooleanOption
}
}
@ApiStatus.Internal
@@ -218,15 +174,6 @@ class SeTargetItemPresentation(
return matchingFragments(text)?.map { SerializableRange(it) }
}
}
override fun contentEquals(other: SeItemPresentation?): Boolean {
if (this === other) return true
if (other !is SeTargetItemPresentation) return false
return super.contentEquals(other) &&
presentableText == other.presentableText &&
containerText == other.containerText &&
locationText == other.locationText
}
}
@ApiStatus.Internal
@@ -240,15 +187,6 @@ class SeTextSearchItemPresentation(
override val isMultiSelectionSupported: Boolean,
) : SeItemPresentation {
val backgroundColor: Color? get() = backgroundColorId?.color()
override fun contentEquals(other: SeItemPresentation?): Boolean {
if (this === other) return true
if (other !is SeTextSearchItemPresentation) return false
return super.contentEquals(other) &&
fileString == other.fileString &&
isMultiSelectionSupported == other.isMultiSelectionSupported
}
}
@ApiStatus.Internal
@@ -259,10 +197,8 @@ class SeAdaptedItemEmptyPresentation(override val isMultiSelectionSupported: Boo
// Must stay internal, it's not serializable and can't be used for transfer
@ApiStatus.Internal
class SeAdaptedItemPresentation(
override val isMultiSelectionSupported: Boolean,
val fetchedItem: Any,
val rendererProvider: () -> ListCellRenderer<Any>,
) : SeItemPresentation {
class SeAdaptedItemPresentation(override val isMultiSelectionSupported: Boolean,
val fetchedItem: Any,
val rendererProvider: () -> ListCellRenderer<Any>) : SeItemPresentation {
override val text: String get() = ""
}