From c2acfa086832057bda057d9e969df6c4d49a89c2 Mon Sep 17 00:00:00 2001 From: Aydar Mukhametzyanov Date: Tue, 30 Sep 2025 14:36:30 +0200 Subject: [PATCH] Revert "IJPL-188268 Search Everywhere: result selection gets lost on the popup reopening" This reverts commit ab2e3edc90eb1148adeba6320a1a878467fbc925. GitOrigin-RevId: 347ce0ebb8b3e916c3c33b415136b3c5a7d2222d --- .../frontend/src/SeFrontendService.kt | 7 +- .../frontend/src/SeSelectionListener.kt | 71 ----------------- .../frontend/src/ui/SePopupContentPane.kt | 79 +++++-------------- .../frontend/src/ui/SeResultJBList.kt | 38 --------- .../shared/api-dump-experimental.txt | 1 - .../searchEverywhere/shared/src/SeItemData.kt | 11 +-- .../shared/src/SeItemPresentation.kt | 70 +--------------- 7 files changed, 25 insertions(+), 252 deletions(-) delete mode 100644 platform/searchEverywhere/frontend/src/SeSelectionListener.kt delete mode 100644 platform/searchEverywhere/frontend/src/ui/SeResultJBList.kt diff --git a/platform/searchEverywhere/frontend/src/SeFrontendService.kt b/platform/searchEverywhere/frontend/src/SeFrontendService.kt index 2ccd860dbd11..4c497b687eac 100644 --- a/platform/searchEverywhere/frontend/src/SeFrontendService.kt +++ b/platform/searchEverywhere/frontend/src/SeFrontendService.kt @@ -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 -> diff --git a/platform/searchEverywhere/frontend/src/SeSelectionListener.kt b/platform/searchEverywhere/frontend/src/SeSelectionListener.kt deleted file mode 100644 index a2a89381f208..000000000000 --- a/platform/searchEverywhere/frontend/src/SeSelectionListener.kt +++ /dev/null @@ -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, - 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) diff --git a/platform/searchEverywhere/frontend/src/ui/SePopupContentPane.kt b/platform/searchEverywhere/frontend/src/ui/SePopupContentPane.kt index a2dbcfdc514f..6bed1d12de40 100644 --- a/platform/searchEverywhere/frontend/src/ui/SePopupContentPane.kt +++ b/platform/searchEverywhere/frontend/src/ui/SePopupContentPane.kt @@ -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 = SeResultJBList(resultListModel) - private var selectionListener = SeSelectionListener(initialSelectionState, resultList, resultListModel) + private val resultList: JBList = 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 { diff --git a/platform/searchEverywhere/frontend/src/ui/SeResultJBList.kt b/platform/searchEverywhere/frontend/src/ui/SeResultJBList.kt deleted file mode 100644 index 56cea1bffb6b..000000000000 --- a/platform/searchEverywhere/frontend/src/ui/SeResultJBList.kt +++ /dev/null @@ -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(model: ListModel) : JBList(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 - } -} \ No newline at end of file diff --git a/platform/searchEverywhere/shared/api-dump-experimental.txt b/platform/searchEverywhere/shared/api-dump-experimental.txt index 3b335f113a40..76b3ab736af7 100644 --- a/platform/searchEverywhere/shared/api-dump-experimental.txt +++ b/platform/searchEverywhere/shared/api-dump-experimental.txt @@ -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 diff --git a/platform/searchEverywhere/shared/src/SeItemData.kt b/platform/searchEverywhere/shared/src/SeItemData.kt index 7edd2ddd8784..cdde74b48b10 100644 --- a/platform/searchEverywhere/shared/src/SeItemData.kt +++ b/platform/searchEverywhere/shared/src/SeItemData.kt @@ -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) - } } diff --git a/platform/searchEverywhere/shared/src/SeItemPresentation.kt b/platform/searchEverywhere/shared/src/SeItemPresentation.kt index abf79ab16b74..ee5ed691d88e 100644 --- a/platform/searchEverywhere/shared/src/SeItemPresentation.kt +++ b/platform/searchEverywhere/shared/src/SeItemPresentation.kt @@ -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, -) : SeItemPresentation { +class SeAdaptedItemPresentation(override val isMultiSelectionSupported: Boolean, + val fetchedItem: Any, + val rendererProvider: () -> ListCellRenderer) : SeItemPresentation { override val text: String get() = "" }