From dfff0327aaa290204572fbcf524f075b51291085 Mon Sep 17 00:00:00 2001 From: Mikhail Sokolov Date: Wed, 14 Oct 2020 19:09:29 +0300 Subject: [PATCH] removing duplicated SearchEverywhereUIMixedResults GitOrigin-RevId: 413ac34011ca7e2bb87b8b37517def29599125a0 --- .../tests/community/focus/FocusIssuesUtil.kt | 4 +- .../navigation/SearchEverywhereTest.groovy | 11 +- .../AbstractGotoSEContributor.java | 2 +- .../searcheverywhere/GroupedListFactory.java | 102 ++ ...rcher.java => GroupedResultsSearcher.java} | 25 +- .../GroupedSearchListModel.java | 219 +++ .../searcheverywhere/MixedListFactory.java | 92 ++ ...earcher.java => MixedResultsSearcher.java} | 16 +- .../MixedSearchListModel.java | 162 ++ .../SEListSelectionTracker.java | 5 +- .../SEResultsListFactory.java | 45 + .../actions/searcheverywhere/SESearcher.java | 5 +- .../SearchEverywhereHeader.java | 8 +- .../SearchEverywhereManagerImpl.java | 10 +- .../searcheverywhere/SearchEverywhereUI.java | 481 ++---- .../SearchEverywhereUIBase.java | 84 - .../searcheverywhere/SearchListModel.java | 100 ++ .../ThrottlingListenerWrapper.java | 6 +- .../mixed/SEListSelectionTracker.java | 109 -- .../searcheverywhere/mixed/SESearcher.java | 29 - .../mixed/SearchEverywhereUIMixedResults.java | 1415 ----------------- .../mixed/ThrottlingListenerWrapper.java | 142 -- .../MixingMultiThreadSearchTest.groovy | 2 +- .../MultiThreadSearchDeadlockTest.java | 6 +- .../MultiThreadSearchTest.java | 4 +- .../searcheverywhere/SearchModelTest.java | 6 +- 26 files changed, 869 insertions(+), 2221 deletions(-) create mode 100644 platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/GroupedListFactory.java rename platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/{MultiThreadSearcher.java => GroupedResultsSearcher.java} (95%) create mode 100644 platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/GroupedSearchListModel.java create mode 100644 platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MixedListFactory.java rename platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/{mixed/MultiThreadSearcher.java => MixedResultsSearcher.java} (95%) create mode 100644 platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MixedSearchListModel.java create mode 100644 platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SEResultsListFactory.java delete mode 100644 platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereUIBase.java create mode 100644 platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchListModel.java delete mode 100644 platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SEListSelectionTracker.java delete mode 100644 platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SESearcher.java delete mode 100644 platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SearchEverywhereUIMixedResults.java delete mode 100644 platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/ThrottlingListenerWrapper.java diff --git a/community-guitests/testSrc/com/intellij/testGuiFramework/tests/community/focus/FocusIssuesUtil.kt b/community-guitests/testSrc/com/intellij/testGuiFramework/tests/community/focus/FocusIssuesUtil.kt index f6b9de593c72..f40755d72af9 100644 --- a/community-guitests/testSrc/com/intellij/testGuiFramework/tests/community/focus/FocusIssuesUtil.kt +++ b/community-guitests/testSrc/com/intellij/testGuiFramework/tests/community/focus/FocusIssuesUtil.kt @@ -1,7 +1,7 @@ // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.testGuiFramework.tests.community.focus -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereUIBase +import com.intellij.ide.actions.searcheverywhere.SearchEverywhereUI import com.intellij.testGuiFramework.framework.Timeouts.defaultTimeout import com.intellij.testGuiFramework.framework.GuiTestUtil.textfield import com.intellij.testGuiFramework.impl.GuiRobotHolder @@ -58,7 +58,7 @@ object FocusIssuesUtil { } private fun findSearchEverywhereUI(): Container { - return GuiRobotHolder.robot.finder().find { it is SearchEverywhereUIBase } as SearchEverywhereUIBase + return GuiRobotHolder.robot.finder().find { it is SearchEverywhereUI } as SearchEverywhereUI } } \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/navigation/SearchEverywhereTest.groovy b/java/java-tests/testSrc/com/intellij/java/navigation/SearchEverywhereTest.groovy index 341c68aadabd..a139fd90d7f5 100644 --- a/java/java-tests/testSrc/com/intellij/java/navigation/SearchEverywhereTest.groovy +++ b/java/java-tests/testSrc/com/intellij/java/navigation/SearchEverywhereTest.groovy @@ -2,7 +2,6 @@ package com.intellij.java.navigation import com.intellij.ide.actions.searcheverywhere.* -import com.intellij.ide.actions.searcheverywhere.mixed.SearchEverywhereUIMixedResults import com.intellij.ide.util.gotoByName.GotoActionTest import com.intellij.openapi.actionSystem.AbbreviationManager import com.intellij.openapi.actionSystem.ActionManager @@ -25,7 +24,7 @@ import static com.intellij.testFramework.PlatformTestUtil.waitForFuture class SearchEverywhereTest extends LightJavaCodeInsightFixtureTestCase { static final int SEARCH_TIMEOUT = 50_000 - SearchEverywhereUIBase mySearchUI + SearchEverywhereUI mySearchUI private SEParam mixingResultsFlag private SEParam twoTabsFlag @@ -241,18 +240,16 @@ class SearchEverywhereTest extends LightJavaCodeInsightFixtureTestCase { assert waitForFuture(future, SEARCH_TIMEOUT) == [file4, file3, file5, file2, file1, file6] } - private SearchEverywhereUIBase createTestUI(List> contributors) { + private SearchEverywhereUI createTestUI(List> contributors) { def map = new HashMap, SearchEverywhereTabDescriptor>() contributors.forEach({map.put(it, null)}) return createTestUI(map) } - private SearchEverywhereUIBase createTestUI(Map, SearchEverywhereTabDescriptor> contributorsMap) { + private SearchEverywhereUI createTestUI(Map, SearchEverywhereTabDescriptor> contributorsMap) { if (mySearchUI != null) Disposer.dispose(mySearchUI) - def mixingEnabled = Experiments.getInstance().isFeatureEnabled("search.everywhere.mixed.results") - mySearchUI = mixingEnabled ? new SearchEverywhereUIMixedResults(project, contributorsMap) - : new SearchEverywhereUI(project, contributorsMap) + mySearchUI = new SearchEverywhereUI(project, contributorsMap) def tab = contributorsMap.size() > 1 ? SearchEverywhereManagerImpl.ALL_CONTRIBUTORS_GROUP_ID : contributorsMap.keySet().find().getSearchProviderId() diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/AbstractGotoSEContributor.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/AbstractGotoSEContributor.java index e711a0835029..66519e705e57 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/AbstractGotoSEContributor.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/AbstractGotoSEContributor.java @@ -220,7 +220,7 @@ public abstract class AbstractGotoSEContributor implements WeightedSearchEverywh } }); } - result.add(new SearchEverywhereUIBase.FiltersAction(filter, onChanged)); + result.add(new SearchEverywhereUI.FiltersAction(filter, onChanged)); return result; } diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/GroupedListFactory.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/GroupedListFactory.java new file mode 100644 index 000000000000..286b22267998 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/GroupedListFactory.java @@ -0,0 +1,102 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ide.actions.searcheverywhere; + +import com.intellij.ide.actions.SearchEverywhereClassifier; +import com.intellij.ide.util.gotoByName.GotoActionModel; +import com.intellij.ui.AppUIUtil; +import com.intellij.ui.CellRendererPanel; +import com.intellij.ui.SeparatorComponent; +import com.intellij.ui.SimpleColoredComponent; +import com.intellij.ui.components.JBList; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.Nls; + +import javax.swing.*; +import javax.swing.border.Border; +import java.awt.*; + +class GroupedListFactory extends SEResultsListFactory { + @Override + public SearchListModel createModel() { + return new GroupedSearchListModel(); + } + + @Override + public JBList createList(SearchListModel model) { + return new JBList<>(model); + } + + @Override + ListCellRenderer createListRenderer(SearchListModel model, SearchEverywhereHeader header) { + GroupedSearchListModel groupedModel = (GroupedSearchListModel)model; + return new ListCellRenderer<>() { + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + if (value == SearchListModel.MORE_ELEMENT) { + Component component = myMoreRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + component.setPreferredSize(UIUtil.updateListRowHeight(component.getPreferredSize())); + return component; + } + + SearchEverywhereContributor contributor = groupedModel.getContributorForIndex(index); + Component component = SearchEverywhereClassifier.EP_Manager.getListCellRendererComponent( + list, value, index, isSelected, cellHasFocus); + if (component == null) { + //noinspection ConstantConditions + component = contributor.getElementsRenderer().getListCellRendererComponent( + list, value, index, isSelected, true); + } + + if (component instanceof JComponent) { + Border border = ((JComponent)component).getBorder(); + if (border != GotoActionModel.GotoActionListCellRenderer.TOGGLE_BUTTON_BORDER) { + ((JComponent)component).setBorder(JBUI.Borders.empty(1, 2)); + } + } + AppUIUtil.targetToDevice(component, list); + component.setPreferredSize(UIUtil.updateListRowHeight(component.getPreferredSize())); + if (!header.getSelectedTab().isSingleContributor() && groupedModel.isGroupFirstItem(index)) { + //noinspection ConstantConditions + component = myGroupTitleRenderer.withDisplayedData(contributor.getFullGroupName(), component); + } + + return component; + } + }; + } + + private final GroupTitleRenderer myGroupTitleRenderer = new GroupTitleRenderer(); + + private static class GroupTitleRenderer extends CellRendererPanel { + + final SimpleColoredComponent titleLabel = new SimpleColoredComponent(); + + GroupTitleRenderer() { + setLayout(new BorderLayout()); + SeparatorComponent separatorComponent = new SeparatorComponent( + titleLabel.getPreferredSize().height / 2, JBUI.CurrentTheme.BigPopup.listSeparatorColor(), null); + + JPanel topPanel = JBUI.Panels.simplePanel(5, 0) + .addToCenter(separatorComponent) + .addToLeft(titleLabel) + .withBorder(JBUI.Borders.empty(1, 7)) + .withBackground(UIUtil.getListBackground()); + add(topPanel, BorderLayout.NORTH); + } + + public GroupTitleRenderer withDisplayedData(@Nls String title, Component itemContent) { + titleLabel.clear(); + titleLabel.append(title, SMALL_LABEL_ATTRS); + Component prevContent = ((BorderLayout)getLayout()).getLayoutComponent(BorderLayout.CENTER); + if (prevContent != null) { + remove(prevContent); + } + add(itemContent, BorderLayout.CENTER); + accessibleContext = itemContent.getAccessibleContext(); + + return this; + } + } +} diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MultiThreadSearcher.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/GroupedResultsSearcher.java similarity index 95% rename from platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MultiThreadSearcher.java rename to platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/GroupedResultsSearcher.java index b27ab7b3c2ac..4892345b965f 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MultiThreadSearcher.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/GroupedResultsSearcher.java @@ -25,9 +25,9 @@ import static com.intellij.ide.actions.searcheverywhere.SEResultsEqualityProvide /** * @author msokolov */ -class MultiThreadSearcher implements SESearcher { +class GroupedResultsSearcher implements SESearcher { - private static final Logger LOG = Logger.getInstance(MultiThreadSearcher.class); + private static final Logger LOG = Logger.getInstance(GroupedResultsSearcher.class); @NotNull private final Listener myListener; @NotNull private final Executor myNotificationExecutor; @@ -40,9 +40,9 @@ class MultiThreadSearcher implements SESearcher { * @param notificationExecutor searcher guarantees that all listener methods will be called only through this executor * @param equalityProviders collection of equality providers that checks if found elements are already in the search results */ - MultiThreadSearcher(@NotNull Listener listener, - @NotNull Executor notificationExecutor, - @NotNull Collection equalityProviders) { + GroupedResultsSearcher(@NotNull Listener listener, + @NotNull Executor notificationExecutor, + @NotNull Collection equalityProviders) { myListener = listener; myNotificationExecutor = notificationExecutor; myEqualityProvider = SEResultsEqualityProvider.composite(equalityProviders); @@ -98,9 +98,16 @@ class MultiThreadSearcher implements SESearcher { @Override public ProgressIndicator findMoreItems(@NotNull Map, Collection> alreadyFound, - @NotNull String pattern, - @NotNull SearchEverywhereContributor contributor, - int newLimit) { + @NotNull Map, Integer> contributorsAndLimits, + @NotNull String pattern) { + if (contributorsAndLimits.size() > 1) + throw new IllegalArgumentException("Multiple contributors are not allowed for grouped list"); + + Map.Entry, Integer> entry = contributorsAndLimits.entrySet().stream().findFirst() + .orElseThrow(() -> new IllegalArgumentException("Empty contributors map is not allowed")); + SearchEverywhereContributor contributor = entry.getKey(); + int newLimit = entry.getValue(); + ProgressIndicator indicator = new ProgressIndicatorBase(); ResultsAccumulator accumulator = new ShowMoreResultsAccumulator(alreadyFound, myEqualityProvider, contributor, newLimit, myListener, myNotificationExecutor, indicator); @@ -223,7 +230,7 @@ class MultiThreadSearcher implements SESearcher { private static abstract class ResultsAccumulator { protected final Map, Collection> sections; - protected final MultiThreadSearcher.Listener myListener; + protected final SESearcher.Listener myListener; protected final Executor myNotificationExecutor; protected final SEResultsEqualityProvider myEqualityProvider; protected final ProgressIndicator myProgressIndicator; diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/GroupedSearchListModel.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/GroupedSearchListModel.java new file mode 100644 index 000000000000..3046894fb8af --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/GroupedSearchListModel.java @@ -0,0 +1,219 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ide.actions.searcheverywhere; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.util.ArrayUtil; +import com.intellij.util.diff.Diff; +import com.intellij.util.diff.FilesTooBigForDiffException; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +class GroupedSearchListModel extends SearchListModel { + + private static final Logger LOG = Logger.getInstance(GroupedSearchListModel.class); + + @Override + public boolean hasMoreElements(SearchEverywhereContributor contributor) { + return listElements.stream() + .anyMatch(info -> info.getElement() == MORE_ELEMENT && info.getContributor() == contributor); + } + + @Override + public void addElements(List items) { + if (items.isEmpty()) { + return; + } + Map, List> itemsMap = new HashMap<>(); + items.forEach(info -> { + List list = itemsMap.computeIfAbsent(info.getContributor(), contributor -> new ArrayList<>()); + list.add(info); + }); + itemsMap.forEach((contributor, list) -> list.sort(Comparator.comparingInt(SearchEverywhereFoundElementInfo::getPriority).reversed())); + + if (resultsExpired) { + retainContributors(itemsMap.keySet()); + clearMoreItems(); + + itemsMap.forEach((contributor, list) -> { + Object[] oldItems = ArrayUtil.toObjectArray(getFoundItems(contributor)); + Object[] newItems = list.stream() + .map(SearchEverywhereFoundElementInfo::getElement) + .toArray(); + try { + Diff.Change change = Diff.buildChanges(oldItems, newItems); + applyChange(change, contributor, list); + } + catch (FilesTooBigForDiffException e) { + LOG.error("Cannot calculate diff for updated search results"); + } + }); + resultsExpired = false; + } + else { + itemsMap.forEach((contributor, list) -> { + int startIndex = contributors().indexOf(contributor); + int insertionIndex = getInsertionPoint(contributor); + int endIndex = insertionIndex + list.size() - 1; + listElements.addAll(insertionIndex, list); + fireIntervalAdded(this, insertionIndex, endIndex); + + // there were items for this contributor before update + if (startIndex >= 0) { + listElements.subList(startIndex, endIndex + 1) + .sort(Comparator.comparingInt(SearchEverywhereFoundElementInfo::getPriority).reversed()); + fireContentsChanged(this, startIndex, endIndex); + } + }); + } + } + + private void retainContributors(Collection> retainContributors) { + Iterator iterator = listElements.iterator(); + int startInterval = 0; + int endInterval = -1; + while (iterator.hasNext()) { + SearchEverywhereFoundElementInfo item = iterator.next(); + if (retainContributors.contains(item.getContributor())) { + if (startInterval <= endInterval) { + fireIntervalRemoved(this, startInterval, endInterval); + startInterval = endInterval + 2; + } + else { + startInterval++; + } + } + else { + iterator.remove(); + } + endInterval++; + } + + if (startInterval <= endInterval) { + fireIntervalRemoved(this, startInterval, endInterval); + } + } + + @Override + public void clearMoreItems() { + ListIterator iterator = listElements.listIterator(); + while (iterator.hasNext()) { + int index = iterator.nextIndex(); + if (iterator.next().getElement() == MORE_ELEMENT) { + iterator.remove(); + fireContentsChanged(this, index, index); + } + } + } + + private void applyChange(Diff.Change change, + SearchEverywhereContributor contributor, + List newItems) { + int firstItemIndex = contributors().indexOf(contributor); + if (firstItemIndex < 0) { + firstItemIndex = getInsertionPoint(contributor); + } + + for (Diff.Change ch : toRevertedList(change)) { + if (ch.deleted > 0) { + for (int i = ch.deleted - 1; i >= 0; i--) { + int index = firstItemIndex + ch.line0 + i; + listElements.remove(index); + } + fireIntervalRemoved(this, firstItemIndex + ch.line0, firstItemIndex + ch.line0 + ch.deleted - 1); + } + + if (ch.inserted > 0) { + List addedItems = newItems.subList(ch.line1, ch.line1 + ch.inserted); + listElements.addAll(firstItemIndex + ch.line0, addedItems); + fireIntervalAdded(this, firstItemIndex + ch.line0, firstItemIndex + ch.line0 + ch.inserted - 1); + } + } + } + + private static List toRevertedList(Diff.Change change) { + List res = new ArrayList<>(); + while (change != null) { + res.add(0, change); + change = change.link; + } + return res; + } + + @Override + public void removeElement(@NotNull Object item, SearchEverywhereContributor contributor) { + int index = contributors().indexOf(contributor); + if (index < 0) { + return; + } + + while (index < getSize() && listElements.get(index).getContributor() == contributor) { + if (item.equals(getElementAt(index))) { + listElements.remove(index); + fireIntervalRemoved(this, index, index); + return; + } + index++; + } + } + + @Override + public void setHasMore(SearchEverywhereContributor contributor, boolean newVal) { + int index = contributors().lastIndexOf(contributor); + if (index < 0) { + return; + } + + boolean alreadyHas = isMoreElement(index); + if (alreadyHas && !newVal) { + listElements.remove(index); + fireIntervalRemoved(this, index, index); + } + + if (!alreadyHas && newVal) { + index += 1; + listElements.add(index, new SearchEverywhereFoundElementInfo(MORE_ELEMENT, 0, contributor)); + fireIntervalAdded(this, index, index); + } + } + + public boolean isGroupFirstItem(int index) { + return index == 0 || listElements.get(index).getContributor() != listElements.get(index - 1).getContributor(); + } + + @Override + public int getIndexToScroll(int currentIndex, boolean scrollDown) { + int index = currentIndex; + do { + index += scrollDown ? 1 : -1; + } + while (index >= 0 && index < getSize() && !isGroupFirstItem(index) && !isMoreElement(index)); + + return Integer.max(Integer.min(index, getSize() - 1), 0); + } + + public int getItemsForContributor(SearchEverywhereContributor contributor) { + List contributorsList = contributors(); + int first = contributorsList.indexOf(contributor); + int last = contributorsList.lastIndexOf(contributor); + if (isMoreElement(last)) { + last -= 1; + } + return last - first + 1; + } + + private int getInsertionPoint(SearchEverywhereContributor contributor) { + if (listElements.isEmpty()) { + return 0; + } + + List list = contributors(); + int index = list.lastIndexOf(contributor); + if (index >= 0) { + return isMoreElement(index) ? index : index + 1; + } + + index = Collections.binarySearch(list, contributor, Comparator.comparingInt(SearchEverywhereContributor::getSortWeight)); + return -index - 1; + } +} diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MixedListFactory.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MixedListFactory.java new file mode 100644 index 000000000000..ba3cf3622fe1 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MixedListFactory.java @@ -0,0 +1,92 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ide.actions.searcheverywhere; + +import com.intellij.ide.actions.SearchEverywhereClassifier; +import com.intellij.ide.util.gotoByName.GotoActionModel; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.ui.AppUIUtil; +import com.intellij.ui.components.JBList; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.UIUtil; + +import javax.swing.*; +import javax.swing.border.Border; +import java.awt.*; +import java.util.*; +import java.util.List; + +class MixedListFactory extends SEResultsListFactory { + + private final List prioritizedContributors = new ArrayList<>(); + + MixedListFactory() { + prioritizedContributors.add("CommandsContributor"); + prioritizedContributors.add(TopHitSEContributor.class.getSimpleName()); + if (Registry.is("search.everywhere.recent.at.top")) { + prioritizedContributors.add(RecentFilesSEContributor.class.getSimpleName()); + } + } + + @Override + public SearchListModel createModel() { + MixedSearchListModel mixedModel = new MixedSearchListModel(); + + Map priorities = new HashMap<>(); + for (int i = 0; i < prioritizedContributors.size(); i++) { + priorities.put(prioritizedContributors.get(i), prioritizedContributors.size() - i); + } + + Comparator prioritizedContributorsComparator = (element1, element2) -> { + int firstElementPriority = priorities.getOrDefault(element1.getContributor().getSearchProviderId(), 0); + int secondElementPriority = priorities.getOrDefault(element2.getContributor().getSearchProviderId(), 0); + return Integer.compare(firstElementPriority, secondElementPriority); + }; + + Comparator comparator = prioritizedContributorsComparator + .thenComparing(SearchEverywhereFoundElementInfo.COMPARATOR) + .reversed(); + mixedModel.setElementsComparator(comparator); + + return mixedModel; + } + + @Override + public JBList createList(SearchListModel model) { + return new JBList<>(model); + } + + @Override + ListCellRenderer createListRenderer(SearchListModel model, SearchEverywhereHeader header) { + return new ListCellRenderer<>() { + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + if (value == SearchListModel.MORE_ELEMENT) { + Component component = myMoreRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + component.setPreferredSize(UIUtil.updateListRowHeight(component.getPreferredSize())); + return component; + } + + SearchEverywhereContributor contributor = model.getContributorForIndex(index); + Component component = SearchEverywhereClassifier.EP_Manager.getListCellRendererComponent( + list, value, index, isSelected, cellHasFocus); + if (component == null) { + //noinspection ConstantConditions + component = contributor.getElementsRenderer().getListCellRendererComponent( + list, value, index, isSelected, true); + } + + if (component instanceof JComponent) { + Border border = ((JComponent)component).getBorder(); + if (border != GotoActionModel.GotoActionListCellRenderer.TOGGLE_BUTTON_BORDER) { + ((JComponent)component).setBorder(JBUI.Borders.empty(1, 2)); + } + } + AppUIUtil.targetToDevice(component, list); + component.setPreferredSize(UIUtil.updateListRowHeight(component.getPreferredSize())); + + return component; + } + }; + } +} diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/MultiThreadSearcher.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MixedResultsSearcher.java similarity index 95% rename from platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/MultiThreadSearcher.java rename to platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MixedResultsSearcher.java index 8cea7884a5f2..1b5c1b033d06 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/MultiThreadSearcher.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MixedResultsSearcher.java @@ -1,11 +1,7 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.ide.actions.searcheverywhere.mixed; +package com.intellij.ide.actions.searcheverywhere; import com.intellij.concurrency.SensitiveProgressWrapper; -import com.intellij.ide.actions.searcheverywhere.SEResultsEqualityProvider; -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereContributor; -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereFoundElementInfo; -import com.intellij.ide.actions.searcheverywhere.WeightedSearchEverywhereContributor; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; @@ -29,9 +25,9 @@ import static com.intellij.ide.actions.searcheverywhere.SEResultsEqualityProvide /** * @author msokolov */ -class MultiThreadSearcher implements SESearcher { +class MixedResultsSearcher implements SESearcher { - private static final Logger LOG = Logger.getInstance(MultiThreadSearcher.class); + private static final Logger LOG = Logger.getInstance(MixedResultsSearcher.class); @NotNull private final Listener myListener; @NotNull private final Executor myNotificationExecutor; @@ -44,9 +40,9 @@ class MultiThreadSearcher implements SESearcher { * @param notificationExecutor searcher guarantees that all listener methods will be called only through this executor * @param equalityProviders collection of equality providers that checks if found elements are already in the search results */ - MultiThreadSearcher(@NotNull Listener listener, - @NotNull Executor notificationExecutor, - @NotNull Collection equalityProviders) { + MixedResultsSearcher(@NotNull Listener listener, + @NotNull Executor notificationExecutor, + @NotNull Collection equalityProviders) { myListener = listener; myNotificationExecutor = notificationExecutor; myEqualityProvider = SEResultsEqualityProvider.composite(equalityProviders); diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MixedSearchListModel.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MixedSearchListModel.java new file mode 100644 index 000000000000..f3b464368591 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/MixedSearchListModel.java @@ -0,0 +1,162 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ide.actions.searcheverywhere; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.util.ArrayUtil; +import com.intellij.util.diff.Diff; +import com.intellij.util.diff.FilesTooBigForDiffException; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Collectors; + +class MixedSearchListModel extends SearchListModel { + + private static final Logger LOG = Logger.getInstance(MixedSearchListModel.class); + + private final Map, Boolean> hasMoreContributors = new HashMap<>(); + + private Comparator myElementsComparator = SearchEverywhereFoundElementInfo.COMPARATOR.reversed(); + + // new elements cannot be added before this index when "more..." elements are loaded + private int myMaxFrozenIndex; + + public void setElementsComparator(Comparator elementsComparator) { + myElementsComparator = elementsComparator; + } + + @Override + public boolean hasMoreElements(SearchEverywhereContributor contributor) { + return Boolean.TRUE.equals(hasMoreContributors.get(contributor)); + } + + @Override + public int getIndexToScroll(int currentIndex, boolean scrollDown) { + return scrollDown ? getSize() - 1 : 0; + } + + @Override + public void addElements(List items) { + if (items.isEmpty()) { + return; + } + + items = items.stream() + .sorted(myElementsComparator) + .collect(Collectors.toList()); + + if (resultsExpired) { + clearMoreItems(); + + Object[] oldItems = ArrayUtil.toObjectArray(getItems()); + Object[] newItems = items.stream() + .map(SearchEverywhereFoundElementInfo::getElement) + .toArray(); + try { + Diff.Change change = Diff.buildChanges(oldItems, newItems); + applyChange(change, items); + } + catch (FilesTooBigForDiffException e) { + LOG.error("Cannot calculate diff for updated search results"); + } + + resultsExpired = false; + } + else { + int startIndex = listElements.size(); + listElements.addAll(items); + int endIndex = listElements.size() - 1; + fireIntervalAdded(this, startIndex, endIndex); + + // there were items for this contributor before update + if (startIndex > 0) { + List lst = myMaxFrozenIndex >= 0 + ? listElements.subList(myMaxFrozenIndex + 1, listElements.size()) + : listElements; + lst.sort(myElementsComparator); + fireContentsChanged(this, 0, endIndex); + } + } + } + + @Override + public void clearMoreItems() { + if (listElements.isEmpty()) return; + + int lastItemIndex = listElements.size() - 1; + SearchEverywhereFoundElementInfo lastItem = listElements.get(lastItemIndex); + if (lastItem.getElement() == MORE_ELEMENT) { + listElements.remove(lastItemIndex); + fireIntervalRemoved(this, lastItemIndex, lastItemIndex); + } + } + + private void applyChange(Diff.Change change, List newItems) { + for (Diff.Change ch : toRevertedList(change)) { + if (ch.deleted > 0) { + listElements.subList(ch.line0, ch.line0 + ch.deleted).clear(); + fireIntervalRemoved(this, ch.line0, ch.line0 + ch.deleted - 1); + } + + if (ch.inserted > 0) { + List addedItems = newItems.subList(ch.line1, ch.line1 + ch.inserted); + listElements.addAll(ch.line0, addedItems); + fireIntervalAdded(this, ch.line0, ch.line0 + ch.inserted - 1); + } + } + } + + private static List toRevertedList(Diff.Change change) { + List res = new ArrayList<>(); + while (change != null) { + res.add(0, change); + change = change.link; + } + return res; + } + + @Override + public void removeElement(@NotNull Object item, SearchEverywhereContributor contributor) { + if (listElements.isEmpty()) return; + + for (int i = 0; i < listElements.size(); i++) { + SearchEverywhereFoundElementInfo info = listElements.get(i); + if (info.getContributor() == contributor && info.getElement().equals(item)) { + listElements.remove(i); + fireIntervalRemoved(this, i, i); + return; + } + } + } + + @Override + public void setHasMore(SearchEverywhereContributor contributor, boolean contributorHasMore) { + hasMoreContributors.put(contributor, contributorHasMore); + + int lasItemIndex = listElements.size() - 1; + if (lasItemIndex < 0) { + return; + } + + boolean hasMore = hasMoreContributors.values().stream().anyMatch(val -> val); + boolean alreadyHas = isMoreElement(lasItemIndex); + if (alreadyHas && !hasMore) { + listElements.remove(lasItemIndex); + fireIntervalRemoved(this, lasItemIndex, lasItemIndex); + } + + if (!alreadyHas && hasMore) { + myMaxFrozenIndex = lasItemIndex; + listElements.add(new SearchEverywhereFoundElementInfo(MORE_ELEMENT, 0, null)); + lasItemIndex += 1; + fireIntervalAdded(this, lasItemIndex, lasItemIndex); + } + } + + @Override + public void clear() { + hasMoreContributors.clear(); + myMaxFrozenIndex = -1; + super.clear(); + } +} diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SEListSelectionTracker.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SEListSelectionTracker.java index 01d8ab0e4a05..4dfbe8c04ee4 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SEListSelectionTracker.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SEListSelectionTracker.java @@ -18,13 +18,13 @@ import java.util.stream.IntStream; class SEListSelectionTracker implements ListSelectionListener { private final JBList myList; - private final SearchEverywhereUI.SearchListModel myListModel; + private final SearchListModel myListModel; private int myLockCounter; private final List mySelectedItems = new ArrayList<>(); private boolean myMoreSelected; - SEListSelectionTracker(@NotNull JBList list, @NotNull SearchEverywhereUI.SearchListModel model) { + SEListSelectionTracker(@NotNull JBList list, @NotNull SearchListModel model) { myList = list; myListModel = model; } @@ -32,6 +32,7 @@ class SEListSelectionTracker implements ListSelectionListener { @Override public void valueChanged(ListSelectionEvent e) { if (isLocked()) return; + if (myList.getSelectedIndices().length == 0) return; saveSelection(); } diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SEResultsListFactory.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SEResultsListFactory.java new file mode 100644 index 000000000000..054df751511f --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SEResultsListFactory.java @@ -0,0 +1,45 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ide.actions.searcheverywhere; + +import com.intellij.ide.IdeBundle; +import com.intellij.ui.ColoredListCellRenderer; +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.ui.components.JBList; +import com.intellij.util.ui.JBInsets; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +abstract class SEResultsListFactory { + + abstract SearchListModel createModel(); + + abstract JBList createList(SearchListModel model); + + abstract ListCellRenderer createListRenderer(SearchListModel model, SearchEverywhereHeader header); + + protected static final SimpleTextAttributes SMALL_LABEL_ATTRS = new SimpleTextAttributes( + SimpleTextAttributes.STYLE_SMALLER, JBUI.CurrentTheme.BigPopup.listTitleLabelForeground()); + + protected static final ListCellRenderer myMoreRenderer = new ColoredListCellRenderer<>() { + + @Override + protected int getMinHeight() { + return -1; + } + + @Override + protected void customizeCellRenderer(@NotNull JList list, Object value, int index, boolean selected, boolean hasFocus) { + if (value != SearchListModel.MORE_ELEMENT) { + throw new AssertionError(value); + } + setFont(UIUtil.getLabelFont().deriveFont(UIUtil.getFontSize(UIUtil.FontSize.SMALL))); + append(IdeBundle.message("search.everywhere.points.more"), SMALL_LABEL_ATTRS); + setIpad(JBInsets.create(1, 7)); + setMyBorder(null); + } + }; + +} diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SESearcher.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SESearcher.java index 466f2572f52f..cc8725718ed8 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SESearcher.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SESearcher.java @@ -13,9 +13,8 @@ interface SESearcher { @NotNull String pattern); ProgressIndicator findMoreItems(@NotNull Map, Collection> alreadyFound, - @NotNull String pattern, - @NotNull SearchEverywhereContributor contributor, - int newLimit); + @NotNull Map, Integer> contributorsAndLimits, + @NotNull String pattern); /** * Search process listener interface diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereHeader.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereHeader.java index da62fba8791d..fe581a83b0cf 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereHeader.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereHeader.java @@ -55,7 +55,7 @@ public class SearchEverywhereHeader { public SearchEverywhereHeader(@Nullable Project project, Map, SearchEverywhereTabDescriptor> contributors, @NotNull Runnable scopeChangedCallback, Function shortcutSupplier, - AnAction showInFindToolWindowAction, SearchEverywhereUIBase ui) { + AnAction showInFindToolWindowAction, SearchEverywhereUI ui) { myScopeChangedCallback = scopeChangedCallback; myProject = project; myShortcutSupplier = shortcutSupplier; @@ -149,7 +149,7 @@ public class SearchEverywhereHeader { if (myProject != null && !projectContributors.isEmpty()) { List projectActions = Arrays.asList( new MyScopeChooserAction(myProject, projectContributors, myScopeChangedCallback), - new SearchEverywhereUIBase.FiltersAction(createContributorsFilter(myProject, projectContributors), myScopeChangedCallback) + new SearchEverywhereUI.FiltersAction(createContributorsFilter(myProject, projectContributors), myScopeChangedCallback) ); res.add(createTab(SearchEverywhereTabDescriptor.PROJECT.getId(), IdeBundle.message("searcheverywhere.project.search.tab.name"), projectContributors, projectActions)); @@ -176,7 +176,7 @@ public class SearchEverywhereHeader { myScopeChangedCallback.run(); } }, - new SearchEverywhereUIBase.FiltersAction(createContributorsFilter(myProject, ideContributors), myScopeChangedCallback) + new SearchEverywhereUI.FiltersAction(createContributorsFilter(myProject, ideContributors), myScopeChangedCallback) ); res.add(createTab(SearchEverywhereTabDescriptor.IDE.getId(), IdeBundle.message("searcheverywhere.ide.search.tab.name"), ideContributors, ideActions)); @@ -211,7 +211,7 @@ public class SearchEverywhereHeader { }); onChanged.run(); } - }, new SearchEverywhereUIBase.FiltersAction(createContributorsFilter(myProject, contributors), onChanged)); + }, new SearchEverywhereUI.FiltersAction(createContributorsFilter(myProject, contributors), onChanged)); SETab allTab = createTab(SearchEverywhereManagerImpl.ALL_CONTRIBUTORS_GROUP_ID, IdeBundle.message("searcheverywhere.allelements.tab.name"), contributors, actions); diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereManagerImpl.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereManagerImpl.java index cd1b5f5d04ab..aa07f496c278 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereManagerImpl.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereManagerImpl.java @@ -3,13 +3,11 @@ package com.intellij.ide.actions.searcheverywhere; import com.intellij.codeWithMe.ClientId; import com.intellij.ide.actions.BigPopupUI; -import com.intellij.ide.actions.searcheverywhere.mixed.SearchEverywhereUIMixedResults; import com.intellij.ide.actions.searcheverywhere.statistics.SearchEverywhereUsageTriggerCollector; import com.intellij.ide.lightEdit.LightEdit; import com.intellij.ide.lightEdit.LightEditCompatible; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.Experiments; import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; @@ -52,7 +50,7 @@ public final class SearchEverywhereManagerImpl implements SearchEverywhereManage private final Project myProject; private JBPopup myBalloon; - private SearchEverywhereUIBase mySearchEverywhereUI; + private SearchEverywhereUI mySearchEverywhereUI; private Dimension myBalloonFullSize; private final SearchHistoryList myHistoryList = new SearchHistoryList(); @@ -256,14 +254,12 @@ public final class SearchEverywhereManagerImpl implements SearchEverywhereManage myEverywhere = everywhere; } - private SearchEverywhereUIBase createView(Project project, + private SearchEverywhereUI createView(Project project, Map, SearchEverywhereTabDescriptor> contributors) { if (LightEdit.owns(project)) { contributors = ContainerUtil.filter(contributors, (contributor) -> contributor instanceof LightEditCompatible); } - SearchEverywhereUIBase view = Experiments.getInstance().isFeatureEnabled("search.everywhere.mixed.results") - ? new SearchEverywhereUIMixedResults(project, contributors, myTabsShortcutsMap::get) - : new SearchEverywhereUI(project, contributors, myTabsShortcutsMap::get); + SearchEverywhereUI view = new SearchEverywhereUI(project, contributors, myTabsShortcutsMap::get); view.setSearchFinishedHandler(() -> { if (isShown()) { diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereUI.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereUI.java index a72f1dad33af..238839b7794e 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereUI.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereUI.java @@ -1,24 +1,24 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.ide.actions.searcheverywhere; -import com.google.common.collect.Lists; import com.intellij.find.findUsages.PsiElement2UsageTargetAdapter; import com.intellij.icons.AllIcons; import com.intellij.ide.IdeBundle; import com.intellij.ide.SearchTopHitProvider; -import com.intellij.ide.actions.SearchEverywhereClassifier; +import com.intellij.ide.actions.BigPopupUI; +import com.intellij.ide.actions.bigPopup.ShowFilterAction; import com.intellij.ide.actions.searcheverywhere.SearchEverywhereHeader.SETab; import com.intellij.ide.actions.searcheverywhere.statistics.SearchEverywhereUsageTriggerCollector; import com.intellij.ide.actions.searcheverywhere.statistics.SearchFieldStatisticsCollector; -import com.intellij.ide.util.gotoByName.GotoActionModel; +import com.intellij.ide.util.ElementsChooser; import com.intellij.ide.util.gotoByName.QuickSearchComponent; import com.intellij.internal.statistic.eventLog.FeatureUsageData; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.impl.ActionMenu; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ApplicationNamesInfo; +import com.intellij.openapi.application.Experiments; import com.intellij.openapi.application.ReadAction; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; @@ -32,6 +32,7 @@ import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.NlsContexts; import com.intellij.openapi.wm.ToolWindowId; import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.psi.PsiElement; @@ -48,16 +49,12 @@ import com.intellij.usageView.UsageInfo; import com.intellij.usages.*; import com.intellij.usages.impl.UsageViewManagerImpl; import com.intellij.util.Alarm; -import com.intellij.util.ArrayUtil; import com.intellij.util.Consumer; import com.intellij.util.Processor; import com.intellij.util.containers.ContainerUtil; -import com.intellij.util.diff.Diff; -import com.intellij.util.diff.FilesTooBigForDiffException; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.text.MatcherHolder; import com.intellij.util.ui.EmptyIcon; -import com.intellij.util.ui.JBInsets; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.Nls; @@ -66,7 +63,6 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import javax.swing.*; -import javax.swing.border.Border; import javax.swing.event.DocumentEvent; import java.awt.*; import java.awt.event.*; @@ -78,6 +74,7 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; import static com.intellij.ide.actions.searcheverywhere.statistics.SearchEverywhereUsageTriggerCollector.getReportableContributorID; @@ -85,26 +82,21 @@ import static com.intellij.ide.actions.searcheverywhere.statistics.SearchEverywh * @author Konstantin Bulenkov * @author Mikhail.Sokolov */ -public final class SearchEverywhereUI extends SearchEverywhereUIBase implements DataProvider, QuickSearchComponent { - private static final Logger LOG = Logger.getInstance(SearchEverywhereUI.class); +public final class SearchEverywhereUI extends BigPopupUI implements DataProvider, QuickSearchComponent { + + public static final String SEARCH_EVERYWHERE_SEARCH_FILED_KEY = "search-everywhere-textfield"; //only for testing purposes public static final int SINGLE_CONTRIBUTOR_ELEMENTS_LIMIT = 30; public static final int MULTIPLE_CONTRIBUTORS_ELEMENTS_LIMIT = 15; public static final int THROTTLING_TIMEOUT = 100; - private static final SimpleTextAttributes SMALL_LABEL_ATTRS = new SimpleTextAttributes( - SimpleTextAttributes.STYLE_SMALLER, JBUI.CurrentTheme.BigPopup.listTitleLabelForeground()); - + private final SEResultsListFactory myListFactory; private SearchListModel myListModel; private final SearchEverywhereHeader myHeader; - private JBPopup myHint; - private String myNotFoundString; - private final SESearcher mySearcher; private final ThrottlingListenerWrapper myBufferedListener; private ProgressIndicator mySearchProgressIndicator; - private final SEListSelectionTracker mySelectionTracker; public SearchEverywhereUI(@Nullable Project project, @@ -116,10 +108,16 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements Map, SearchEverywhereTabDescriptor> contributors, @NotNull Function shortcutSupplier) { super(project); + myListFactory = Experiments.getInstance().isFeatureEnabled("search.everywhere.mixed.results") + ? new MixedListFactory() + : new GroupedListFactory(); + List equalityProviders = SEResultsEqualityProvider.getProviders(); myBufferedListener = new ThrottlingListenerWrapper(THROTTLING_TIMEOUT, mySearchListener, Runnable::run); - mySearcher = new MultiThreadSearcher(myBufferedListener, run -> - ApplicationManager.getApplication().invokeLater(run), equalityProviders); + mySearcher = Experiments.getInstance().isFeatureEnabled("search.everywhere.mixed.results") + ? new MixedResultsSearcher(myBufferedListener, run -> ApplicationManager.getApplication().invokeLater(run), equalityProviders) + : new GroupedResultsSearcher(myBufferedListener, run -> ApplicationManager.getApplication().invokeLater(run), equalityProviders); + Runnable scopeChangedCallback = () -> { updateSearchFieldAdvertisement(); scheduleRebuildList(); @@ -136,7 +134,10 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements int[] selectedIndices = myResultsList.getSelectedIndices(); if (selectedIndices.length > 1) { boolean multiSelection = Arrays.stream(selectedIndices) - .allMatch(i -> myListModel.getContributorForIndex(i).isMultiSelectionSupported()); + .allMatch(i -> { + SearchEverywhereContributor contributor = myListModel.getContributorForIndex(i); + return contributor != null && contributor.isMultiSelectionSupported(); + }); if (!multiSelection) { int index = myResultsList.getLeadSelectionIndex(); myResultsList.setSelectedIndex(index); @@ -157,24 +158,21 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements return (list, value, index, isSelected, cellHasFocus) -> new JPanel(); } - return new CompositeCellRenderer(); + return myListFactory.createListRenderer(myListModel, myHeader); } @NotNull @Override public JBList createList() { - myListModel = new SearchListModel(); + myListModel = myListFactory.createModel(); addListDataListener(myListModel); - - return new JBList<>(myListModel); + return myListFactory.createList(myListModel); } - @Override public void toggleEverywhereFilter() { myHeader.toggleEverywhere(); } - @Override public void switchToTab(@NotNull String tabID) { SETab selectedTab = myHeader.getTabs().stream() .filter(tab -> tab.getID().equals(tabID)) @@ -228,12 +226,10 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements } } - @Override public String getSelectedTabID() { return myHeader.getSelectedTab().getID(); } - @Override @Nullable public Object getSelectionIdentity() { Object value = myResultsList.getSelectedValue(); @@ -264,6 +260,7 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements List elements = indicesStream.mapToObj(i -> { SearchEverywhereContributor contributor = myListModel.getContributorForIndex(i); Object item = myListModel.getElementAt(i); + //noinspection ConstantConditions Object psi = contributor.getDataForItem(item, CommonDataKeys.PSI_ELEMENT.getName()); return (PsiElement)psi; }) @@ -276,6 +273,7 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements return indicesStream.mapToObj(i -> { SearchEverywhereContributor contributor = myListModel.getContributorForIndex(i); Object item = myListModel.getElementAt(i); + //noinspection ConstantConditions return contributor.getDataForItem(item, dataId); }) .filter(Objects::nonNull) @@ -318,6 +316,7 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements @NotNull @Override + @NlsContexts.PopupAdvertisement protected String[] getInitialHints() { return new String[]{ IdeBundle.message("searcheverywhere.open.in.split.shortcuts.hint", @@ -503,14 +502,14 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements triggerTabSwitched(e); }); registerAction(SearchEverywhereActions.NAVIGATE_TO_NEXT_GROUP, e -> { - fetchGroups(true); + scrollList(true); FeatureUsageData data = SearchEverywhereUsageTriggerCollector .createData(null) .addInputEvent(e); featureTriggered(SearchEverywhereUsageTriggerCollector.GROUP_NAVIGATE, data); }); registerAction(SearchEverywhereActions.NAVIGATE_TO_PREV_GROUP, e -> { - fetchGroups(false); + scrollList(false); FeatureUsageData data = SearchEverywhereUsageTriggerCollector .createData(null) .addInputEvent(e); @@ -578,6 +577,7 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements private void showDescriptionForIndex(int index) { if (index >= 0 && !myListModel.isMoreElement(index)) { SearchEverywhereContributor contributor = myListModel.getContributorForIndex(index); + //noinspection ConstantConditions Object data = contributor.getDataForItem( myListModel.getElementAt(index), SearchEverywhereDataKeys.ITEM_STRING_DESCRIPTION.getName()); if (data instanceof String) { @@ -645,18 +645,13 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements featureTriggered(SearchEverywhereUsageTriggerCollector.TAB_SWITCHED, data); } - private void fetchGroups(boolean down) { - int index = myResultsList.getSelectedIndex(); - do { - index += down ? 1 : -1; - } - while (index >= 0 && - index < myListModel.getSize() && - !myListModel.isGroupFirstItem(index) && - !myListModel.isMoreElement(index)); - if (index >= 0 && index < myListModel.getSize()) { - myResultsList.setSelectedIndex(index); - ScrollingUtil.ensureIndexIsVisible(myResultsList, index, 0); + private void scrollList(boolean down) { + int currentIndex = myResultsList.getSelectedIndex(); + int newIndex = myListModel.getIndexToScroll(currentIndex, down); + + if (newIndex != currentIndex) { + myResultsList.setSelectedIndex(newIndex); + ScrollingUtil.ensureIndexIsVisible(myResultsList, newIndex, 0); } } @@ -733,6 +728,7 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements Object value = myListModel.getElementAt(i); String selectedTabContributorID = myHeader.getSelectedTab().getReportableID(); + //noinspection ConstantConditions String reportableContributorID = getReportableContributorID(contributor); FeatureUsageData data = SearchEverywhereUsageTriggerCollector.createData(reportableContributorID, selectedTabContributorID, i); if (value instanceof PsiElement) { @@ -753,10 +749,30 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements private void showMoreElements(SearchEverywhereContributor contributor) { featureTriggered(SearchEverywhereUsageTriggerCollector.MORE_ITEM_SELECTED, null); + + if (contributor != null) { + myListModel.setHasMore(contributor, false); + } + else { + myListModel.clearMoreItems(); + } + Map, Collection> found = myListModel.getFoundElementsMap(); - int limit = myListModel.getItemsForContributor(contributor) - + (myHeader.getSelectedTab().isSingleContributor() ? SINGLE_CONTRIBUTOR_ELEMENTS_LIMIT : MULTIPLE_CONTRIBUTORS_ELEMENTS_LIMIT); - mySearchProgressIndicator = mySearcher.findMoreItems(found, getSearchPattern(), contributor, limit); + int additionalItemsCount = myHeader.getSelectedTab().isSingleContributor() ? SINGLE_CONTRIBUTOR_ELEMENTS_LIMIT + : MULTIPLE_CONTRIBUTORS_ELEMENTS_LIMIT; + + Stream, Collection>> stream = found.entrySet().stream(); + if (contributor != null) { + stream = stream.filter(entry -> entry.getKey() == contributor); + } + else { + stream = stream.filter(entry -> myListModel.hasMoreElements(entry.getKey())); + } + + Map, Integer> contributorsAndLimits = + stream.collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().size() + additionalItemsCount)); + + mySearchProgressIndicator = mySearcher.findMoreItems(found, contributorsAndLimits, getSearchPattern()); } private void stopSearching() { @@ -774,7 +790,6 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements searchFinishedHandler.run(); } - @Override @TestOnly public Future> findElementsForPattern(String pattern) { clearResults(); @@ -787,47 +802,12 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements return future; } - @Override @TestOnly public void clearResults() { myListModel.clear(); mySearchField.setText(""); } - private class CompositeCellRenderer implements ListCellRenderer { - - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { - if (value == SearchListModel.MORE_ELEMENT) { - Component component = myMoreRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - component.setPreferredSize(UIUtil.updateListRowHeight(component.getPreferredSize())); - return component; - } - - SearchEverywhereContributor contributor = myListModel.getContributorForIndex(index); - Component component = SearchEverywhereClassifier.EP_Manager.getListCellRendererComponent( - list, value, index, isSelected, cellHasFocus); - if (component == null) { - component = contributor.getElementsRenderer().getListCellRendererComponent( - list, value, index, isSelected, true); - } - - if (component instanceof JComponent) { - Border border = ((JComponent)component).getBorder(); - if (border != GotoActionModel.GotoActionListCellRenderer.TOGGLE_BUTTON_BORDER) { - ((JComponent)component).setBorder(JBUI.Borders.empty(1, 2)); - } - } - AppUIUtil.targetToDevice(component, list); - component.setPreferredSize(UIUtil.updateListRowHeight(component.getPreferredSize())); - if (!myHeader.getSelectedTab().isSingleContributor() && myListModel.isGroupFirstItem(index)) { - component = myGroupTitleRenderer.withDisplayedData(contributor.getFullGroupName(), component); - } - - return component; - } - } - private final ListCellRenderer myCommandRenderer = new ColoredListCellRenderer<>() { @Override @@ -843,320 +823,53 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements } }; - private final ListCellRenderer myMoreRenderer = new ColoredListCellRenderer<>() { + protected static class FiltersAction extends ShowFilterAction { + final PersistentSearchEverywhereContributorFilter filter; + final Runnable rebuildRunnable; - @Override - protected int getMinHeight() { - return -1; + public FiltersAction(@NotNull PersistentSearchEverywhereContributorFilter filter, + @NotNull Runnable rebuildRunnable) { + this.filter = filter; + this.rebuildRunnable = rebuildRunnable; } @Override - protected void customizeCellRenderer(@NotNull JList list, Object value, int index, boolean selected, boolean hasFocus) { - if (value != SearchListModel.MORE_ELEMENT) { - throw new AssertionError(value); - } - setFont(UIUtil.getLabelFont().deriveFont(UIUtil.getFontSize(UIUtil.FontSize.SMALL))); - append(IdeBundle.message("search.everywhere.points.more"), SMALL_LABEL_ATTRS); - setIpad(JBInsets.create(1, 7)); - setMyBorder(null); - } - }; - - - private final GroupTitleRenderer myGroupTitleRenderer = new GroupTitleRenderer(); - - private static class GroupTitleRenderer extends CellRendererPanel { - - final SimpleColoredComponent titleLabel = new SimpleColoredComponent(); - - GroupTitleRenderer() { - setLayout(new BorderLayout()); - SeparatorComponent separatorComponent = new SeparatorComponent( - titleLabel.getPreferredSize().height / 2, JBUI.CurrentTheme.BigPopup.listSeparatorColor(), null); - - JPanel topPanel = JBUI.Panels.simplePanel(5, 0) - .addToCenter(separatorComponent) - .addToLeft(titleLabel) - .withBorder(JBUI.Borders.empty(1, 7)) - .withBackground(UIUtil.getListBackground()); - add(topPanel, BorderLayout.NORTH); - } - - public GroupTitleRenderer withDisplayedData(@Nls String title, Component itemContent) { - titleLabel.clear(); - titleLabel.append(title, SMALL_LABEL_ATTRS); - Component prevContent = ((BorderLayout)getLayout()).getLayoutComponent(BorderLayout.CENTER); - if (prevContent != null) { - remove(prevContent); - } - add(itemContent, BorderLayout.CENTER); - accessibleContext = itemContent.getAccessibleContext(); - - return this; - } - } - - public static class SearchListModel extends AbstractListModel { - - static final Object MORE_ELEMENT = new Object(); - - private final List listElements = new ArrayList<>(); - - private boolean resultsExpired = false; - - public boolean isResultsExpired() { - return resultsExpired; - } - - public void expireResults() { - resultsExpired = true; + public boolean isEnabled() { + return true; } @Override - public int getSize() { - return listElements.size(); + protected boolean isActive() { + return filter.getAllElements().size() != filter.getSelectedElements().size(); } @Override - public Object getElementAt(int index) { - return listElements.get(index).getElement(); + protected ElementsChooser createChooser() { + return createChooser(filter, rebuildRunnable); } - public List getItems() { - return new ArrayList<>(values()); - } - - public Collection getFoundItems(SearchEverywhereContributor contributor) { - return listElements.stream() - .filter(info -> info.getContributor() == contributor && info.getElement() != MORE_ELEMENT) - .map(info -> info.getElement()) - .collect(Collectors.toList()); - } - - public boolean hasMoreElements(SearchEverywhereContributor contributor) { - return listElements.stream() - .anyMatch(info -> info.getElement() == MORE_ELEMENT && info.getContributor() == contributor); - } - - public void addElements(List items) { - if (items.isEmpty()) { - return; - } - Map, List> itemsMap = new HashMap<>(); - items.forEach(info -> { - List list = itemsMap.computeIfAbsent(info.getContributor(), contributor -> new ArrayList<>()); - list.add(info); - }); - itemsMap.forEach((contributor, list) -> list.sort(Comparator.comparingInt(SearchEverywhereFoundElementInfo::getPriority).reversed())); - - if (resultsExpired) { - retainContributors(itemsMap.keySet()); - clearMoreItems(); - - itemsMap.forEach((contributor, list) -> { - Object[] oldItems = ArrayUtil.toObjectArray(getFoundItems(contributor)); - Object[] newItems = list.stream() - .map(SearchEverywhereFoundElementInfo::getElement) - .toArray(); - try { - Diff.Change change = Diff.buildChanges(oldItems, newItems); - applyChange(change, contributor, list); - } - catch (FilesTooBigForDiffException e) { - LOG.error("Cannot calculate diff for updated search results"); - } - }); - resultsExpired = false; - } - else { - itemsMap.forEach((contributor, list) -> { - int startIndex = contributors().indexOf(contributor); - int insertionIndex = getInsertionPoint(contributor); - int endIndex = insertionIndex + list.size() - 1; - listElements.addAll(insertionIndex, list); - fireIntervalAdded(this, insertionIndex, endIndex); - - // there were items for this contributor before update - if (startIndex >= 0) { - listElements.subList(startIndex, endIndex + 1) - .sort(Comparator.comparingInt(SearchEverywhereFoundElementInfo::getPriority).reversed()); - fireContentsChanged(this, startIndex, endIndex); - } - }); - } - } - - private void retainContributors(Collection> retainContributors) { - Iterator iterator = listElements.iterator(); - int startInterval = 0; - int endInterval = -1; - while (iterator.hasNext()) { - SearchEverywhereFoundElementInfo item = iterator.next(); - if (retainContributors.contains(item.getContributor())) { - if (startInterval <= endInterval) { - fireIntervalRemoved(this, startInterval, endInterval); - startInterval = endInterval + 2; - } - else { - startInterval++; - } - } - else { - iterator.remove(); - } - endInterval++; - } - - if (startInterval <= endInterval) { - fireIntervalRemoved(this, startInterval, endInterval); - } - } - - private void clearMoreItems() { - ListIterator iterator = listElements.listIterator(); - while (iterator.hasNext()) { - int index = iterator.nextIndex(); - if (iterator.next().getElement() == MORE_ELEMENT) { - iterator.remove(); - fireContentsChanged(this, index, index); - } - } - } - - private void applyChange(Diff.Change change, - SearchEverywhereContributor contributor, - List newItems) { - int firstItemIndex = contributors().indexOf(contributor); - if (firstItemIndex < 0) { - firstItemIndex = getInsertionPoint(contributor); - } - - for (Diff.Change ch : toRevertedList(change)) { - if (ch.deleted > 0) { - for (int i = ch.deleted - 1; i >= 0; i--) { - int index = firstItemIndex + ch.line0 + i; - listElements.remove(index); - } - fireIntervalRemoved(this, firstItemIndex + ch.line0, firstItemIndex + ch.line0 + ch.deleted - 1); + private static ElementsChooser createChooser(@NotNull PersistentSearchEverywhereContributorFilter filter, + @NotNull Runnable rebuildRunnable) { + ElementsChooser res = new ElementsChooser<>(filter.getAllElements(), false) { + @Override + protected String getItemText(@NotNull T value) { + return filter.getElementText(value); } - if (ch.inserted > 0) { - List addedItems = newItems.subList(ch.line1, ch.line1 + ch.inserted); - listElements.addAll(firstItemIndex + ch.line0, addedItems); - fireIntervalAdded(this, firstItemIndex + ch.line0, firstItemIndex + ch.line0 + ch.inserted - 1); + @Nullable + @Override + protected Icon getItemIcon(@NotNull T value) { + return filter.getElementIcon(value); } - } - } - - private static List toRevertedList(Diff.Change change) { - List res = new ArrayList<>(); - while (change != null) { - res.add(0, change); - change = change.link; - } + }; + res.markElements(filter.getSelectedElements()); + ElementsChooser.ElementsMarkListener listener = (element, isMarked) -> { + filter.setSelected(element, isMarked); + rebuildRunnable.run(); + }; + res.addElementsMarkListener(listener); return res; } - - public void removeElement(@NotNull Object item, SearchEverywhereContributor contributor) { - int index = contributors().indexOf(contributor); - if (index < 0) { - return; - } - - while (index < listElements.size() && listElements.get(index).getContributor() == contributor) { - if (item.equals(listElements.get(index).getElement())) { - listElements.remove(index); - fireIntervalRemoved(this, index, index); - return; - } - index++; - } - } - - public void setHasMore(SearchEverywhereContributor contributor, boolean newVal) { - int index = contributors().lastIndexOf(contributor); - if (index < 0) { - return; - } - - boolean alreadyHas = isMoreElement(index); - if (alreadyHas && !newVal) { - listElements.remove(index); - fireIntervalRemoved(this, index, index); - } - - if (!alreadyHas && newVal) { - index += 1; - listElements.add(index, new SearchEverywhereFoundElementInfo(MORE_ELEMENT, 0, contributor)); - fireIntervalAdded(this, index, index); - } - } - - public void clear() { - int index = listElements.size() - 1; - listElements.clear(); - if (index >= 0) { - fireIntervalRemoved(this, 0, index); - } - } - - public boolean contains(Object val) { - return values().contains(val); - } - - public boolean isMoreElement(int index) { - return listElements.get(index).getElement() == MORE_ELEMENT; - } - - public SearchEverywhereContributor getContributorForIndex(int index) { - //noinspection unchecked - return (SearchEverywhereContributor)listElements.get(index).getContributor(); - } - - public boolean isGroupFirstItem(int index) { - return index == 0 || listElements.get(index).getContributor() != listElements.get(index - 1).getContributor(); - } - - public int getItemsForContributor(SearchEverywhereContributor contributor) { - List contributorsList = contributors(); - int first = contributorsList.indexOf(contributor); - int last = contributorsList.lastIndexOf(contributor); - if (isMoreElement(last)) { - last -= 1; - } - return last - first + 1; - } - - public Map, Collection> getFoundElementsMap() { - return listElements.stream() - .filter(info -> info.element != MORE_ELEMENT) - .collect(Collectors.groupingBy(o -> o.getContributor(), Collectors.toCollection(ArrayList::new))); - } - - @NotNull - private List contributors() { - return Lists.transform(listElements, info -> info.getContributor()); - } - - @NotNull - private List values() { - return Lists.transform(listElements, info -> info.getElement()); - } - - private int getInsertionPoint(SearchEverywhereContributor contributor) { - if (listElements.isEmpty()) { - return 0; - } - - List list = contributors(); - int index = list.lastIndexOf(contributor); - if (index >= 0) { - return isMoreElement(index) ? index : index + 1; - } - - index = Collections.binarySearch(list, contributor, Comparator.comparingInt(SearchEverywhereContributor::getSortWeight)); - return -index - 1; - } } private class ShowInFindToolWindowAction extends DumbAwareAction { @@ -1376,7 +1089,7 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements @Override public void elementsAdded(@NotNull List list) { - boolean wasEmpty = myListModel.listElements.isEmpty(); + boolean wasEmpty = myListModel.getSize() == 0; mySelectionTracker.lock(); myListModel.addElements(list); @@ -1384,13 +1097,13 @@ public final class SearchEverywhereUI extends SearchEverywhereUIBase implements mySelectionTracker.restoreSelection(); - if (wasEmpty && !myListModel.listElements.isEmpty()) { + if (wasEmpty && myListModel.getSize() > 0) { Object prevSelection = ((SearchEverywhereManagerImpl)SearchEverywhereManager.getInstance(myProject)) .getPrevSelection(getSelectedTabID()); if (prevSelection instanceof Integer) { - for (SearchEverywhereFoundElementInfo info : myListModel.listElements) { - if (Objects.hashCode(info.element) == ((Integer)prevSelection).intValue()) { - myResultsList.setSelectedValue(info.element, true); + for (Object item : myListModel.getItems()) { + if (Objects.hashCode(item) == ((Integer)prevSelection).intValue()) { + myResultsList.setSelectedValue(item, true); break; } } diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereUIBase.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereUIBase.java deleted file mode 100644 index 03c8f0a1cbdf..000000000000 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereUIBase.java +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.ide.actions.searcheverywhere; - -import com.intellij.ide.actions.BigPopupUI; -import com.intellij.ide.actions.bigPopup.ShowFilterAction; -import com.intellij.ide.util.ElementsChooser; -import com.intellij.openapi.project.Project; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; - -import javax.swing.*; -import java.util.List; -import java.util.concurrent.Future; - -public abstract class SearchEverywhereUIBase extends BigPopupUI { - public static final String SEARCH_EVERYWHERE_SEARCH_FILED_KEY = "search-everywhere-textfield"; //only for testing purposes - - public SearchEverywhereUIBase(Project project) {super(project);} - - public abstract void toggleEverywhereFilter(); - - public abstract void switchToTab(@NotNull String tabID); - - public abstract String getSelectedTabID(); - - @Nullable - public abstract Object getSelectionIdentity(); - - @TestOnly - public abstract Future> findElementsForPattern(String pattern); - - @TestOnly - public abstract void clearResults(); - - protected static class FiltersAction extends ShowFilterAction { - final PersistentSearchEverywhereContributorFilter filter; - final Runnable rebuildRunnable; - - public FiltersAction(@NotNull PersistentSearchEverywhereContributorFilter filter, - @NotNull Runnable rebuildRunnable) { - this.filter = filter; - this.rebuildRunnable = rebuildRunnable; - } - - @Override - public boolean isEnabled() { - return true; - } - - @Override - protected boolean isActive() { - return filter.getAllElements().size() != filter.getSelectedElements().size(); - } - - @Override - protected ElementsChooser createChooser() { - return createChooser(filter, rebuildRunnable); - } - - private static ElementsChooser createChooser(@NotNull PersistentSearchEverywhereContributorFilter filter, - @NotNull Runnable rebuildRunnable) { - ElementsChooser res = new ElementsChooser(filter.getAllElements(), false) { - @Override - protected String getItemText(@NotNull T value) { - return filter.getElementText(value); - } - - @Nullable - @Override - protected Icon getItemIcon(@NotNull T value) { - return filter.getElementIcon(value); - } - }; - res.markElements(filter.getSelectedElements()); - ElementsChooser.ElementsMarkListener listener = (element, isMarked) -> { - filter.setSelected(element, isMarked); - rebuildRunnable.run(); - }; - res.addElementsMarkListener(listener); - return res; - } - } -} diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchListModel.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchListModel.java new file mode 100644 index 000000000000..3f076999e476 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchListModel.java @@ -0,0 +1,100 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ide.actions.searcheverywhere; + +import com.google.common.collect.Lists; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +abstract class SearchListModel extends AbstractListModel { + + static final Object MORE_ELEMENT = new Object(); + + protected final List listElements = new ArrayList<>(); + protected boolean resultsExpired = false; + + public boolean isResultsExpired() { + return resultsExpired; + } + + public void expireResults() { + resultsExpired = true; + } + + @Override + public int getSize() { + return listElements.size(); + } + + @Override + public Object getElementAt(int index) { + return listElements.get(index).getElement(); + } + + public List getItems() { + return new ArrayList<>(values()); + } + + public Collection getFoundItems(SearchEverywhereContributor contributor) { + return listElements.stream() + .filter(info -> info.getContributor() == contributor && info.getElement() != MORE_ELEMENT) + .map(info -> info.getElement()) + .collect(Collectors.toList()); + } + + @NotNull + protected List contributors() { + return Lists.transform(listElements, info -> info.getContributor()); + } + + @NotNull + protected List values() { + return Lists.transform(listElements, info -> info.getElement()); + } + + public abstract boolean hasMoreElements(SearchEverywhereContributor contributor); + + public abstract void setHasMore(SearchEverywhereContributor contributor, boolean contributorHasMore); + + public abstract void addElements(List items); + + public abstract void removeElement(@NotNull Object item, SearchEverywhereContributor contributor); + + public abstract void clearMoreItems(); + + public abstract int getIndexToScroll(int currentIndex, boolean scrollDown); + + public void clear() { + int index = getSize() - 1; + listElements.clear(); + if (index >= 0) { + fireIntervalRemoved(this, 0, index); + } + } + + public boolean contains(Object val) { + return values().contains(val); + } + + public boolean isMoreElement(int index) { + return getElementAt(index) == MORE_ELEMENT; + } + + @Nullable + public SearchEverywhereContributor getContributorForIndex(int index) { + //noinspection unchecked + return (SearchEverywhereContributor)listElements.get(index).getContributor(); + } + + public Map, Collection> getFoundElementsMap() { + return listElements.stream() + .filter(info -> info.element != MORE_ELEMENT) + .collect(Collectors.groupingBy(o -> o.getContributor(), Collectors.toCollection(ArrayList::new))); + } +} diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/ThrottlingListenerWrapper.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/ThrottlingListenerWrapper.java index 689b702f9ba3..418a5b352345 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/ThrottlingListenerWrapper.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/ThrottlingListenerWrapper.java @@ -10,12 +10,12 @@ import java.util.concurrent.Executor; import java.util.function.BiConsumer; /** - * Implementation of {@link MultiThreadSearcher.Listener} which decrease events rate and raise batch updates + * Implementation of {@link SESearcher.Listener} which decrease events rate and raise batch updates * each {@code throttlingDelay} milliseconds. *
* Not thread-safe and should be notified only in EDT */ -class ThrottlingListenerWrapper implements MultiThreadSearcher.Listener { +class ThrottlingListenerWrapper implements SESearcher.Listener { public final int myThrottlingDelay; @@ -28,7 +28,7 @@ class ThrottlingListenerWrapper implements MultiThreadSearcher.Listener { private final Alarm flushAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); private boolean flushScheduled; - ThrottlingListenerWrapper(int throttlingDelay, MultiThreadSearcher.Listener delegateListener, Executor delegateExecutor) { + ThrottlingListenerWrapper(int throttlingDelay, SESearcher.Listener delegateListener, Executor delegateExecutor) { myThrottlingDelay = throttlingDelay; myDelegateListener = delegateListener; myDelegateExecutor = delegateExecutor; diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SEListSelectionTracker.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SEListSelectionTracker.java deleted file mode 100644 index fe61cea902b9..000000000000 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SEListSelectionTracker.java +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.ide.actions.searcheverywhere.mixed; - -import com.intellij.ui.ScrollingUtil; -import com.intellij.ui.components.JBList; -import com.intellij.util.ArrayUtil; -import org.jetbrains.annotations.NotNull; - -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -class SEListSelectionTracker implements ListSelectionListener { - - private final JBList myList; - private final SearchEverywhereUIMixedResults.SearchListModel myListModel; - - private int myLockCounter; - private final List mySelectedItems = new ArrayList<>(); - private boolean myMoreSelected; - - SEListSelectionTracker(@NotNull JBList list, @NotNull SearchEverywhereUIMixedResults.SearchListModel model) { - myList = list; - myListModel = model; - } - - @Override - public void valueChanged(ListSelectionEvent e) { - if (isLocked()) return; - if (myList.getSelectedIndices().length == 0) return; - - saveSelection(); - } - - void saveSelection() { - mySelectedItems.clear(); - - int[] indices = myList.getSelectedIndices(); - List selectedItemsList; - if (indices.length == 1 && myListModel.isMoreElement(indices[0])) { - myMoreSelected = true; - selectedItemsList = Collections.singletonList(myListModel.getElementAt(indices[0] - 1)); - } - else { - myMoreSelected = false; - selectedItemsList = Arrays.stream(indices) - .filter(i -> !myListModel.isMoreElement(i)) - .mapToObj(i -> myListModel.getElementAt(i)) - .collect(Collectors.toList()); - } - - mySelectedItems.addAll(selectedItemsList); - } - - void restoreSelection() { - if (isLocked()) return; - - lock(); - try { - int[] indicesToSelect = calcIndicesToSelect(); - if (myMoreSelected && indicesToSelect.length == 1) { - indicesToSelect[0] += 1; - } - - if (indicesToSelect.length == 0) { - indicesToSelect = new int[]{0}; - } - - myList.setSelectedIndices(indicesToSelect); - ScrollingUtil.ensureRangeIsVisible(myList, indicesToSelect[0], indicesToSelect[indicesToSelect.length - 1]); - } - finally { - unlock(); - } - } - - void resetSelectionIfNeeded() { - int[] indices = calcIndicesToSelect(); - if (indices.length == 0) { - mySelectedItems.clear(); - } - } - - void lock() { - myLockCounter++; - } - - void unlock() { - if (myLockCounter > 0) myLockCounter--; - } - - private boolean isLocked() { - return myLockCounter > 0; - } - - private int[] calcIndicesToSelect() { - List items = myListModel.getItems(); - if (items.isEmpty()) return ArrayUtil.EMPTY_INT_ARRAY; - - return IntStream.range(0, items.size()) - .filter(i -> mySelectedItems.contains(items.get(i))) - .toArray(); - } -} diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SESearcher.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SESearcher.java deleted file mode 100644 index 6a0949b3e01f..000000000000 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SESearcher.java +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.ide.actions.searcheverywhere.mixed; - -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereContributor; -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereFoundElementInfo; -import com.intellij.openapi.progress.ProgressIndicator; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; -import java.util.List; -import java.util.Map; - -interface SESearcher { - ProgressIndicator search(@NotNull Map, Integer> contributorsAndLimits, - @NotNull String pattern); - - ProgressIndicator findMoreItems(@NotNull Map, Collection> alreadyFound, - @NotNull Map, Integer> contributorsAndLimits, - @NotNull String pattern); - - /** - * Search process listener interface - */ - interface Listener { - void elementsAdded(@NotNull List list); - void elementsRemoved(@NotNull List list); - void searchFinished(@NotNull Map, Boolean> hasMoreContributors); - } -} diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SearchEverywhereUIMixedResults.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SearchEverywhereUIMixedResults.java deleted file mode 100644 index 3793147774c8..000000000000 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SearchEverywhereUIMixedResults.java +++ /dev/null @@ -1,1415 +0,0 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.ide.actions.searcheverywhere.mixed; - -import com.google.common.collect.Lists; -import com.intellij.find.findUsages.PsiElement2UsageTargetAdapter; -import com.intellij.icons.AllIcons; -import com.intellij.ide.IdeBundle; -import com.intellij.ide.SearchTopHitProvider; -import com.intellij.ide.actions.SearchEverywhereClassifier; -import com.intellij.ide.actions.searcheverywhere.*; -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereHeader.SETab; -import com.intellij.ide.actions.searcheverywhere.statistics.SearchEverywhereUsageTriggerCollector; -import com.intellij.ide.actions.searcheverywhere.statistics.SearchFieldStatisticsCollector; -import com.intellij.ide.util.gotoByName.GotoActionModel; -import com.intellij.ide.util.gotoByName.QuickSearchComponent; -import com.intellij.internal.statistic.eventLog.FeatureUsageData; -import com.intellij.openapi.actionSystem.*; -import com.intellij.openapi.actionSystem.impl.ActionMenu; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.ApplicationNamesInfo; -import com.intellij.openapi.application.ReadAction; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.keymap.KeymapUtil; -import com.intellij.openapi.progress.ProcessCanceledException; -import com.intellij.openapi.progress.ProgressIndicator; -import com.intellij.openapi.progress.ProgressManager; -import com.intellij.openapi.progress.Task; -import com.intellij.openapi.progress.util.ProgressIndicatorBase; -import com.intellij.openapi.progress.util.ProgressWindow; -import com.intellij.openapi.progress.util.TooManyUsagesStatus; -import com.intellij.openapi.project.DumbAwareAction; -import com.intellij.openapi.project.DumbService; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.popup.JBPopup; -import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.util.NlsContexts; -import com.intellij.openapi.util.registry.Registry; -import com.intellij.openapi.wm.ToolWindowId; -import com.intellij.openapi.wm.ToolWindowManager; -import com.intellij.psi.PsiElement; -import com.intellij.psi.codeStyle.MinusculeMatcher; -import com.intellij.psi.codeStyle.NameUtil; -import com.intellij.psi.util.PsiUtilCore; -import com.intellij.ui.*; -import com.intellij.ui.components.JBLabel; -import com.intellij.ui.components.JBList; -import com.intellij.ui.components.fields.ExtendableTextField; -import com.intellij.ui.popup.PopupUpdateProcessor; -import com.intellij.ui.scale.JBUIScale; -import com.intellij.usageView.UsageInfo; -import com.intellij.usages.*; -import com.intellij.usages.impl.UsageViewManagerImpl; -import com.intellij.util.Alarm; -import com.intellij.util.ArrayUtil; -import com.intellij.util.Consumer; -import com.intellij.util.Processor; -import com.intellij.util.containers.ContainerUtil; -import com.intellij.util.diff.Diff; -import com.intellij.util.diff.FilesTooBigForDiffException; -import com.intellij.util.messages.MessageBusConnection; -import com.intellij.util.text.MatcherHolder; -import com.intellij.util.ui.EmptyIcon; -import com.intellij.util.ui.JBInsets; -import com.intellij.util.ui.JBUI; -import com.intellij.util.ui.UIUtil; -import org.jetbrains.annotations.Nls; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; - -import javax.swing.*; -import javax.swing.border.Border; -import javax.swing.event.DocumentEvent; -import java.awt.*; -import java.awt.event.*; -import java.util.List; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -/** - * @author Konstantin Bulenkov - * @author Mikhail.Sokolov - */ -public final class SearchEverywhereUIMixedResults extends SearchEverywhereUIBase implements DataProvider, QuickSearchComponent { - private static final Logger LOG = Logger.getInstance(SearchEverywhereUIMixedResults.class); - - public static final int SINGLE_CONTRIBUTOR_ELEMENTS_LIMIT = 30; - public static final int MULTIPLE_CONTRIBUTORS_ELEMENTS_LIMIT = 15; - public static final int THROTTLING_TIMEOUT = 100; - - private static final SimpleTextAttributes SMALL_LABEL_ATTRS = new SimpleTextAttributes( - SimpleTextAttributes.STYLE_SMALLER, JBUI.CurrentTheme.BigPopup.listTitleLabelForeground()); - - private final List prioritizedContributors = new ArrayList<>(); - - private SearchListModel myListModel; - - private final SearchEverywhereHeader myHeader; - - private String myNotFoundString; - - private JBPopup myHint; - - private final SESearcher mySearcher; - private final ThrottlingListenerWrapper myBufferedListener; - private ProgressIndicator mySearchProgressIndicator; - - private final SEListSelectionTracker mySelectionTracker; - - public SearchEverywhereUIMixedResults(@Nullable Project project, - Map, SearchEverywhereTabDescriptor> contributors) { - this(project, contributors, s -> null); - } - - public SearchEverywhereUIMixedResults(@Nullable Project project, - Map, SearchEverywhereTabDescriptor> contributors, - @NotNull Function shortcutSupplier) { - super(project); - List equalityProviders = SEResultsEqualityProvider.getProviders(); - myBufferedListener = new ThrottlingListenerWrapper(THROTTLING_TIMEOUT, mySearchListener, Runnable::run); - mySearcher = new MultiThreadSearcher(myBufferedListener, run -> - ApplicationManager.getApplication().invokeLater(run), equalityProviders); - - Runnable scopeChangedCallback = () -> { - updateSearchFieldAdvertisement(); - scheduleRebuildList(); - }; - myHeader = new SearchEverywhereHeader(project, contributors, scopeChangedCallback, - shortcutSupplier, new ShowInFindToolWindowAction(), this); - - prioritizedContributors.add("CommandsContributor"); - prioritizedContributors.add(TopHitSEContributor.class.getSimpleName()); - if (Registry.is("search.everywhere.recent.at.top")) { - prioritizedContributors.add(RecentFilesSEContributor.class.getSimpleName()); - } - - init(); - - initSearchActions(); - - myResultsList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - myResultsList.addListSelectionListener(e -> { - int[] selectedIndices = myResultsList.getSelectedIndices(); - if (selectedIndices.length > 1) { - boolean multiSelection = Arrays.stream(selectedIndices) - .allMatch(i -> myListModel.getContributorForIndex(i).isMultiSelectionSupported()); - if (!multiSelection) { - int index = myResultsList.getLeadSelectionIndex(); - myResultsList.setSelectedIndex(index); - } - } - }); - - mySelectionTracker = new SEListSelectionTracker(myResultsList, myListModel); - myResultsList.addListSelectionListener(mySelectionTracker); - - Disposer.register(this, SearchFieldStatisticsCollector.createAndStart(mySearchField, myProject)); - } - - @Override - @NotNull - protected ListCellRenderer createCellRenderer() { - if (ApplicationManager.getApplication().isUnitTestMode()) { - return (list, value, index, isSelected, cellHasFocus) -> new JPanel(); - } - - return new CompositeCellRenderer(); - } - - @NotNull - @Override - public JBList createList() { - myListModel = new SearchListModel(); - - Map priorities = new HashMap<>(); - for (int i = 0; i < prioritizedContributors.size(); i++) { - priorities.put(prioritizedContributors.get(i), prioritizedContributors.size() - i); - } - - Comparator prioritizedContributorsComparator = (element1, element2) -> { - int firstElementPriority = priorities.getOrDefault(element1.getContributor().getSearchProviderId(), 0); - int secondElementPriority = priorities.getOrDefault(element2.getContributor().getSearchProviderId(), 0); - return Integer.compare(firstElementPriority, secondElementPriority); - }; - - Comparator comparator = prioritizedContributorsComparator - .thenComparing(SearchEverywhereFoundElementInfo.COMPARATOR) - .reversed(); - myListModel.setElementsComparator(comparator); - - addListDataListener(myListModel); - - return new JBList<>(myListModel); - } - - @Override - public void toggleEverywhereFilter() { - myHeader.toggleEverywhere(); - } - - @Override - public void switchToTab(@NotNull String tabID) { - SETab selectedTab = myHeader.getTabs().stream() - .filter(tab -> tab.getID().equals(tabID)) - .findAny() - .orElseThrow(() -> new IllegalArgumentException(String.format("There is no such tab - %s", tabID))); - switchToTab(selectedTab); - } - - private void switchToTab(SETab tab) { - boolean prevTabIsSingleContributor = myHeader.getSelectedTab().isSingleContributor(); - myHeader.switchToTab(tab); - boolean nextTabIsSingleContributor = myHeader.getSelectedTab().isSingleContributor(); - - updateSearchFieldAdvertisement(); - - if (prevTabIsSingleContributor != nextTabIsSingleContributor) { - //reset cell renderer to show/hide group titles in "All" tab - myResultsList.setCellRenderer(myResultsList.getCellRenderer()); - } - } - - private final JLabel myAdvertisementLabel = new JBLabel(); - { - myAdvertisementLabel.setForeground(JBUI.CurrentTheme.BigPopup.searchFieldGrayForeground()); - myAdvertisementLabel.setFont(RelativeFont.SMALL.derive(getFont())); - } - - private void updateSearchFieldAdvertisement() { - if (mySearchField == null) return; - - List> contributors = myHeader.getSelectedTab().getContributors(); - boolean commandsSupported = contributors.stream() - .anyMatch(contributor -> !contributor.getSupportedCommands().isEmpty()); - - String advertisementText; - if (commandsSupported) { - advertisementText = IdeBundle.message("searcheverywhere.textfield.hint", SearchTopHitProvider.getTopHitAccelerator()); - } - else { - List advertisements = contributors.stream() - .map(c -> c.getAdvertisement()) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - advertisementText = advertisements.isEmpty() ? "" : advertisements.get(new Random().nextInt(advertisements.size())); - } - - mySearchField.remove(myAdvertisementLabel); - if (advertisementText != null) { - myAdvertisementLabel.setText(advertisementText); - mySearchField.add(myAdvertisementLabel, BorderLayout.EAST); - } - } - - @Override - public String getSelectedTabID() { - return myHeader.getSelectedTab().getID(); - } - - @Override - @Nullable - public Object getSelectionIdentity() { - Object value = myResultsList.getSelectedValue(); - return value == null ? null : Objects.hashCode(value); - } - - @Override - public void dispose() { - stopSearching(); - myListModel.clear(); - } - - @Nullable - @Override - public Object getData(@NotNull String dataId) { - IntStream indicesStream = Arrays.stream(myResultsList.getSelectedIndices()) - .filter(i -> !myListModel.isMoreElement(i)); - - //common data section--------------------- - if (PlatformDataKeys.PREDEFINED_TEXT.is(dataId)) { - return getSearchPattern(); - } - if (CommonDataKeys.PROJECT.is(dataId)) { - return myProject; - } - - if (LangDataKeys.PSI_ELEMENT_ARRAY.is(dataId)) { - List elements = indicesStream.mapToObj(i -> { - SearchEverywhereContributor contributor = myListModel.getContributorForIndex(i); - Object item = myListModel.getElementAt(i); - Object psi = contributor.getDataForItem(item, CommonDataKeys.PSI_ELEMENT.getName()); - return (PsiElement)psi; - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - return PsiUtilCore.toPsiElementArray(elements); - } - - //item-specific data section-------------- - return indicesStream.mapToObj(i -> { - SearchEverywhereContributor contributor = myListModel.getContributorForIndex(i); - Object item = myListModel.getElementAt(i); - return contributor.getDataForItem(item, dataId); - }) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - - @Override - public void registerHint(@NotNull JBPopup h) { - if (myHint != null && myHint.isVisible() && myHint != h) { - myHint.cancel(); - } - myHint = h; - } - - @Override - public void unregisterHint() { - myHint = null; - } - - private void hideHint() { - if (myHint != null && myHint.isVisible()) { - myHint.cancel(); - } - } - - private void updateHint(Object element) { - if (myHint == null || !myHint.isVisible()) return; - final PopupUpdateProcessor updateProcessor = myHint.getUserData(PopupUpdateProcessor.class); - if (updateProcessor != null) { - updateProcessor.updatePopup(element); - } - } - - @Override - @NotNull - protected JPanel createSettingsPanel() { - return myHeader.getToolbarPanel(); - } - - @NotNull - @Override - protected @NlsContexts.PopupAdvertisement String[] getInitialHints() { - return new String[]{ - IdeBundle.message("searcheverywhere.open.in.split.shortcuts.hint", - KeymapUtil.getFirstKeyboardShortcutText(IdeActions.ACTION_OPEN_IN_RIGHT_SPLIT)), - IdeBundle.message("searcheverywhere.open.in.new.window.shortcuts.hint", - KeymapUtil.getFirstKeyboardShortcutText(IdeActions.ACTION_EDIT_SOURCE_IN_NEW_WINDOW)), - IdeBundle.message("searcheverywhere.history.shortcuts.hint", - KeymapUtil.getKeystrokeText(SearchTextField.ALT_SHOW_HISTORY_KEYSTROKE), - KeymapUtil.getKeystrokeText(SearchTextField.SHOW_HISTORY_KEYSTROKE))}; - } - - @Override - protected @NotNull String getAccessibleName() { - return IdeBundle.message("searcheverywhere.accessible.name"); - } - - @NotNull - @Override - protected ExtendableTextField createSearchField() { - SearchField res = new SearchField() { - @NotNull - @Override - protected Extension getLeftExtension() { - return new Extension() { - @Override - public Icon getIcon(boolean hovered) { - return AllIcons.Actions.Search; - } - - @Override - public boolean isIconBeforeText() { - return true; - } - - @Override - public int getIconGap() { - return JBUIScale.scale(10); - } - }; - } - }; - res.putClientProperty(SEARCH_EVERYWHERE_SEARCH_FILED_KEY, true); - res.setLayout(new BorderLayout()); - return res; - } - - @Override - protected void installScrollingActions() { - ScrollingUtil.installMoveUpAction(myResultsList, getSearchField()); - ScrollingUtil.installMoveDownAction(myResultsList, getSearchField()); - } - - @Override - @NotNull - protected JPanel createTopLeftPanel() { - return myHeader.getTabsPanel(); - } - - private static final long REBUILD_LIST_DELAY = 100; - private final Alarm rebuildListAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, this); - - private void scheduleRebuildList() { - if (rebuildListAlarm.getActiveRequestCount() == 0) rebuildListAlarm.addRequest(() -> rebuildList(), REBUILD_LIST_DELAY); - } - - private void rebuildList() { - ApplicationManager.getApplication().assertIsDispatchThread(); - - stopSearching(); - - myResultsList.setEmptyText(IdeBundle.message("label.choosebyname.searching")); - String rawPattern = getSearchPattern(); - updateViewType(rawPattern.isEmpty() ? ViewType.SHORT : ViewType.FULL); - String namePattern = myHeader.getSelectedTab().isSingleContributor() - ? myHeader.getSelectedTab().getContributors().get(0).filterControlSymbols(rawPattern) - : rawPattern; - - MinusculeMatcher matcher = - NameUtil.buildMatcherWithFallback("*" + rawPattern, "*" + namePattern, NameUtil.MatchingCaseSensitivity.NONE); - MatcherHolder.associateMatcher(myResultsList, matcher); - - Map, Integer> contributorsMap = new HashMap<>(); - - List> contributors = myHeader.getSelectedTab().getContributors(); - int limit = contributors.size() > 1 ? MULTIPLE_CONTRIBUTORS_ELEMENTS_LIMIT : SINGLE_CONTRIBUTOR_ELEMENTS_LIMIT; - contributors.forEach(c -> contributorsMap.put(c, limit)); - - if (myProject != null) { - contributors = DumbService.getInstance(myProject).filterByDumbAwareness(contributorsMap.keySet()); - if (contributors.isEmpty() && DumbService.isDumb(myProject)) { - myResultsList.setEmptyText(IdeBundle.message("searcheverywhere.indexing.mode.not.supported", - myHeader.getSelectedTab().getText(), - ApplicationNamesInfo.getInstance().getFullProductName())); - myListModel.clear(); - return; - } - if (contributors.size() != contributorsMap.size()) { - myResultsList.setEmptyText(IdeBundle.message("searcheverywhere.indexing.incomplete.results", - myHeader.getSelectedTab().getText(), - ApplicationNamesInfo.getInstance().getFullProductName())); - } - } - - myListModel.expireResults(); - contributors.forEach(contributor -> myListModel.setHasMore(contributor, false)); - String commandPrefix = SearchTopHitProvider.getTopHitAccelerator(); - if (rawPattern.startsWith(commandPrefix)) { - String typedCommand = rawPattern.split(" ")[0].substring(commandPrefix.length()); - List commands = getCommandsForCompletion(contributors, typedCommand); - - if (!commands.isEmpty()) { - if (rawPattern.contains(" ")) { - contributorsMap.keySet().retainAll(commands.stream() - .map(SearchEverywhereCommandInfo::getContributor) - .collect(Collectors.toSet())); - } - else { - myListModel.clear(); - List lst = ContainerUtil.map( - commands, command -> new SearchEverywhereFoundElementInfo(command, 0, myStubCommandContributor)); - myListModel.addElements(lst); - ScrollingUtil.ensureSelectionExists(myResultsList); - } - } - } - mySearchProgressIndicator = mySearcher.search(contributorsMap, rawPattern); - } - - private void initSearchActions() { - MouseAdapter listMouseListener = new MouseAdapter() { - private int currentDescriptionIndex = -1; - - @Override - public void mouseClicked(MouseEvent e) { - onMouseClicked(e); - } - - @Override - public void mouseMoved(MouseEvent e) { - int index = myResultsList.locationToIndex(e.getPoint()); - indexChanged(index); - } - - @Override - public void mouseExited(MouseEvent e) { - int index = myResultsList.getSelectedIndex(); - indexChanged(index); - } - - private void indexChanged(int index) { - if (index != currentDescriptionIndex) { - currentDescriptionIndex = index; - showDescriptionForIndex(index); - } - } - }; - myResultsList.addMouseMotionListener(listMouseListener); - myResultsList.addMouseListener(listMouseListener); - - ScrollingUtil.redirectExpandSelection(myResultsList, mySearchField); - - Consumer nextTabAction = e -> { - myHeader.switchToNextTab(); - triggerTabSwitched(e); - }; - Consumer prevTabAction = e -> { - myHeader.switchToPrevTab(); - triggerTabSwitched(e); - }; - - registerAction(SearchEverywhereActions.AUTOCOMPLETE_COMMAND, CompleteCommandAction::new); - registerAction(SearchEverywhereActions.SWITCH_TO_NEXT_TAB, nextTabAction); - registerAction(SearchEverywhereActions.SWITCH_TO_PREV_TAB, prevTabAction); - registerAction(IdeActions.ACTION_NEXT_TAB, nextTabAction); - registerAction(IdeActions.ACTION_PREVIOUS_TAB, prevTabAction); - registerAction(IdeActions.ACTION_SWITCHER, e -> { - if (e.getInputEvent().isShiftDown()) { - myHeader.switchToPrevTab(); - } - else { - myHeader.switchToNextTab(); - } - triggerTabSwitched(e); - }); - registerAction(SearchEverywhereActions.NAVIGATE_TO_NEXT_GROUP, e -> { - scrollList(true); - FeatureUsageData data = SearchEverywhereUsageTriggerCollector - .createData(null) - .addInputEvent(e); - featureTriggered(SearchEverywhereUsageTriggerCollector.GROUP_NAVIGATE, data); - }); - registerAction(SearchEverywhereActions.NAVIGATE_TO_PREV_GROUP, e -> { - scrollList(false); - FeatureUsageData data = SearchEverywhereUsageTriggerCollector - .createData(null) - .addInputEvent(e); - featureTriggered(SearchEverywhereUsageTriggerCollector.GROUP_NAVIGATE, data); - }); - registerSelectItemAction(); - - AnAction escape = ActionManager.getInstance().getAction("EditorEscape"); - DumbAwareAction.create(__ -> closePopup()) - .registerCustomShortcutSet(escape == null ? CommonShortcuts.ESCAPE : escape.getShortcutSet(), this); - - mySearchField.getDocument().addDocumentListener(new DocumentAdapter() { - @Override - protected void textChanged(@NotNull DocumentEvent e) { - String newSearchString = getSearchPattern(); - if (myNotFoundString != null) { - boolean newPatternContainsPrevious = myNotFoundString.length() > 1 && newSearchString.contains(myNotFoundString); - if (myHeader.canSetEverywhere() && myHeader.isEverywhere() && !newPatternContainsPrevious) { - myNotFoundString = null; - myHeader.autoSetEverywhere(false); - return; - } - } - - scheduleRebuildList(); - } - }); - - myResultsList.addListSelectionListener(e -> { - Object selectedValue = myResultsList.getSelectedValue(); - if (selectedValue != null && myHint != null && myHint.isVisible()) { - updateHint(selectedValue); - } - - showDescriptionForIndex(myResultsList.getSelectedIndex()); - }); - - MessageBusConnection busConnection = myProject != null - ? myProject.getMessageBus().connect(this) - : ApplicationManager.getApplication().getMessageBus().connect(this); - - busConnection.subscribe(DumbService.DUMB_MODE, new DumbService.DumbModeListener() { - @Override - public void exitDumbMode() { - ApplicationManager.getApplication().invokeLater(() -> { - updateSearchFieldAdvertisement(); - scheduleRebuildList(); - }); - } - }); - - busConnection.subscribe(ProgressWindow.TOPIC, pw -> Disposer.register(pw, () -> myResultsList.repaint())); - - mySearchField.addFocusListener(new FocusAdapter() { - @Override - public void focusLost(FocusEvent e) { - Component oppositeComponent = e.getOppositeComponent(); - if (!isHintComponent(oppositeComponent) && !UIUtil.haveCommonOwner(SearchEverywhereUIMixedResults.this, oppositeComponent)) { - closePopup(); - } - } - }); - } - - private void showDescriptionForIndex(int index) { - if (index >= 0 && !myListModel.isMoreElement(index)) { - SearchEverywhereContributor contributor = myListModel.getContributorForIndex(index); - Object data = contributor.getDataForItem( - myListModel.getElementAt(index), SearchEverywhereDataKeys.ITEM_STRING_DESCRIPTION.getName()); - if (data instanceof String) { - ActionMenu.showDescriptionInStatusBar(true, myResultsList, (String)data); - } - } - } - - private void registerAction(String actionID, Supplier actionSupplier) { - Optional.ofNullable(ActionManager.getInstance().getAction(actionID)) - .map(a -> a.getShortcutSet()) - .ifPresent(shortcuts -> actionSupplier.get().registerCustomShortcutSet(shortcuts, this, this)); - } - - private void registerAction(String actionID, Consumer action) { - registerAction(actionID, () -> DumbAwareAction.create(action)); - } - - // when user adds shortcut for "select item" we should add shortcuts - // with all possible modifiers (Ctrl, Shift, Alt, etc.) - private void registerSelectItemAction() { - int[] allowedModifiers = new int[]{ - 0, - InputEvent.SHIFT_MASK, - InputEvent.CTRL_MASK, - InputEvent.META_MASK, - InputEvent.ALT_MASK - }; - - ShortcutSet selectShortcuts = ActionManager.getInstance().getAction(SearchEverywhereActions.SELECT_ITEM).getShortcutSet(); - Collection keyboardShortcuts = Arrays.stream(selectShortcuts.getShortcuts()) - .filter(shortcut -> shortcut instanceof KeyboardShortcut) - .map(shortcut -> (KeyboardShortcut)shortcut) - .collect(Collectors.toList()); - - for (int modifiers : allowedModifiers) { - Collection newShortcuts = new ArrayList<>(); - for (KeyboardShortcut shortcut : keyboardShortcuts) { - boolean hasSecondStroke = shortcut.getSecondKeyStroke() != null; - KeyStroke originalStroke = hasSecondStroke ? shortcut.getSecondKeyStroke() : shortcut.getFirstKeyStroke(); - - if ((originalStroke.getModifiers() & modifiers) != 0) continue; - - KeyStroke newStroke = KeyStroke.getKeyStroke(originalStroke.getKeyCode(), originalStroke.getModifiers() | modifiers); - newShortcuts.add(hasSecondStroke - ? new KeyboardShortcut(shortcut.getFirstKeyStroke(), newStroke) - : new KeyboardShortcut(newStroke, null)); - } - if (newShortcuts.isEmpty()) continue; - - ShortcutSet newShortcutSet = new CustomShortcutSet(newShortcuts.toArray(Shortcut.EMPTY_ARRAY)); - DumbAwareAction.create(event -> { - int[] indices = myResultsList.getSelectedIndices(); - elementsSelected(indices, modifiers); - }).registerCustomShortcutSet(newShortcutSet, this, this); - } - } - - private void triggerTabSwitched(AnActionEvent e) { - String id = myHeader.getSelectedTab().getReportableID(); - - FeatureUsageData data = SearchEverywhereUsageTriggerCollector - .createData(id) - .addInputEvent(e); - featureTriggered(SearchEverywhereUsageTriggerCollector.TAB_SWITCHED, data); - } - - private void scrollList(boolean down) { - int newIndex = down ? myListModel.getSize() - 1 : 0; - int currentIndex = myResultsList.getSelectedIndex(); - - if (newIndex != currentIndex) { - myResultsList.setSelectedIndex(newIndex); - ScrollingUtil.ensureIndexIsVisible(myResultsList, newIndex, 0); - } - } - - private Optional getSelectedCommand(String typedCommand) { - int index = myResultsList.getSelectedIndex(); - if (index < 0) return Optional.empty(); - - SearchEverywhereContributor contributor = myListModel.getContributorForIndex(index); - if (contributor != myStubCommandContributor) return Optional.empty(); - - SearchEverywhereCommandInfo selectedCommand = (SearchEverywhereCommandInfo)myListModel.getElementAt(index); - return selectedCommand.getCommand().contains(typedCommand) ? Optional.of(selectedCommand) : Optional.empty(); - } - - @NotNull - private static List getCommandsForCompletion(Collection> contributors, - String enteredCommandPart) { - Comparator cmdComparator = (cmd1, cmd2) -> { - String cmdName1 = cmd1.getCommand(); - String cmdName2 = cmd2.getCommand(); - if (!enteredCommandPart.isEmpty()) { - if (cmdName1.startsWith(enteredCommandPart) && !cmdName2.startsWith(enteredCommandPart)) return -1; - if (!cmdName1.startsWith(enteredCommandPart) && cmdName2.startsWith(enteredCommandPart)) return 1; - } - - return String.CASE_INSENSITIVE_ORDER.compare(cmdName1, cmd2.getCommand()); - }; - - return contributors.stream() - .flatMap(contributor -> contributor.getSupportedCommands().stream()) - .filter(command -> command.getCommand().contains(enteredCommandPart)) - .sorted(cmdComparator) - .collect(Collectors.toList()); - } - - private void onMouseClicked(@NotNull MouseEvent e) { - boolean multiSelectMode = e.isShiftDown() || UIUtil.isControlKeyDown(e); - if (e.getButton() == MouseEvent.BUTTON1 && !multiSelectMode) { - e.consume(); - final int i = myResultsList.locationToIndex(e.getPoint()); - if (i > -1) { - myResultsList.setSelectedIndex(i); - elementsSelected(new int[]{i}, e.getModifiers()); - } - } - } - - private boolean isHintComponent(Component component) { - if (myHint != null && !myHint.isDisposed() && component != null) { - return SwingUtilities.isDescendingFrom(component, myHint.getContent()); - } - return false; - } - - private void elementsSelected(int[] indexes, int modifiers) { - if (indexes.length == 1 && myListModel.isMoreElement(indexes[0])) { - showMoreElements(); - return; - } - - indexes = Arrays.stream(indexes) - .filter(i -> !myListModel.isMoreElement(i)) - .toArray(); - - String searchText = getSearchPattern(); - if (searchText.startsWith(SearchTopHitProvider.getTopHitAccelerator()) && searchText.contains(" ")) { - featureTriggered(SearchEverywhereUsageTriggerCollector.COMMAND_USED, null); - } - - boolean closePopup = false; - for (int i : indexes) { - SearchEverywhereContributor contributor = myListModel.getContributorForIndex(i); - Object value = myListModel.getElementAt(i); - - String selectedTabID = myHeader.getSelectedTab().getReportableID(); - String reportableContributorID = SearchEverywhereUsageTriggerCollector.getReportableContributorID(contributor); - FeatureUsageData data = SearchEverywhereUsageTriggerCollector.createData(reportableContributorID, selectedTabID, i); - if (value instanceof PsiElement) { - data.addLanguage(((PsiElement) value).getLanguage()); - } - featureTriggered(SearchEverywhereUsageTriggerCollector.CONTRIBUTOR_ITEM_SELECTED, data); - - closePopup |= contributor.processSelectedItem(value, modifiers, searchText); - } - - if (closePopup) { - closePopup(); - } - else { - ApplicationManager.getApplication().invokeLater(() -> myResultsList.repaint()); - } - } - - private void showMoreElements() { - featureTriggered(SearchEverywhereUsageTriggerCollector.MORE_ITEM_SELECTED, null); - - myListModel.clearMoreItem(); - Map, Collection> found = myListModel.getFoundElementsMap(); - int limitAdditional = myHeader.getSelectedTab().isSingleContributor() ? SINGLE_CONTRIBUTOR_ELEMENTS_LIMIT - : MULTIPLE_CONTRIBUTORS_ELEMENTS_LIMIT; - Map, Integer> contributorsAndLimits = found.entrySet().stream() - .filter(entry -> myListModel.hasMoreElements(entry.getKey())) - .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().size() + limitAdditional)); - - myListModel.setMaxFrozenIndex(myListModel.getSize() - 1); - mySearchProgressIndicator = mySearcher.findMoreItems(found, contributorsAndLimits, getSearchPattern()); - } - - private void stopSearching() { - if (mySearchProgressIndicator != null && !mySearchProgressIndicator.isCanceled()) { - mySearchProgressIndicator.cancel(); - } - if (myBufferedListener != null) { - myBufferedListener.clearBuffer(); - } - } - - private void closePopup() { - ActionMenu.showDescriptionInStatusBar(true, myResultsList, null); - stopSearching(); - searchFinishedHandler.run(); - } - - @Override - @TestOnly - public Future> findElementsForPattern(String pattern) { - clearResults(); - CompletableFuture> future = new CompletableFuture<>(); - mySearchListener.setTestCallback(list -> { - future.complete(list); - mySearchListener.setTestCallback(null); - }); - mySearchField.setText(pattern); - return future; - } - - @Override - @TestOnly - public void clearResults() { - myListModel.clear(); - mySearchField.setText(""); - } - - private class CompositeCellRenderer implements ListCellRenderer { - - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { - if (value == SearchListModel.MORE_ELEMENT) { - Component component = myMoreRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - component.setPreferredSize(UIUtil.updateListRowHeight(component.getPreferredSize())); - return component; - } - - SearchEverywhereContributor contributor = myListModel.getContributorForIndex(index); - Component component = SearchEverywhereClassifier.EP_Manager.getListCellRendererComponent( - list, value, index, isSelected, cellHasFocus); - if (component == null) { - component = contributor.getElementsRenderer().getListCellRendererComponent( - list, value, index, isSelected, true); - } - - if (component instanceof JComponent) { - Border border = ((JComponent)component).getBorder(); - if (border != GotoActionModel.GotoActionListCellRenderer.TOGGLE_BUTTON_BORDER) { - ((JComponent)component).setBorder(JBUI.Borders.empty(1, 2)); - } - } - AppUIUtil.targetToDevice(component, list); - component.setPreferredSize(UIUtil.updateListRowHeight(component.getPreferredSize())); - - return component; - } - } - - private final ListCellRenderer myCommandRenderer = new ColoredListCellRenderer<>() { - - @Override - protected void customizeCellRenderer(@NotNull JList list, Object value, int index, boolean selected, boolean hasFocus) { - setPaintFocusBorder(false); - setIcon(EmptyIcon.ICON_16); - setFont(list.getFont()); - - SearchEverywhereCommandInfo command = (SearchEverywhereCommandInfo)value; - append(command.getCommandWithPrefix() + " ", new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, list.getForeground())); - append(command.getDefinition(), new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, JBColor.GRAY)); - setBackground(UIUtil.getListBackground(selected)); - } - }; - - private final ListCellRenderer myMoreRenderer = new ColoredListCellRenderer<>() { - - @Override - protected int getMinHeight() { - return -1; - } - - @Override - protected void customizeCellRenderer(@NotNull JList list, Object value, int index, boolean selected, boolean hasFocus) { - if (value != SearchListModel.MORE_ELEMENT) { - throw new AssertionError(value); - } - setFont(UIUtil.getLabelFont().deriveFont(UIUtil.getFontSize(UIUtil.FontSize.SMALL))); - append(IdeBundle.message("search.everywhere.points.more"), SMALL_LABEL_ATTRS); - setIpad(JBInsets.create(1, 7)); - setMyBorder(null); - } - }; - - public static class SearchListModel extends AbstractListModel { - - static final Object MORE_ELEMENT = new Object(); - - private final List listElements = new ArrayList<>(); - private final Map, Boolean> hasMoreContributors = new HashMap<>(); - - private boolean resultsExpired = false; - private Comparator myElementsComparator = SearchEverywhereFoundElementInfo.COMPARATOR.reversed(); - - // new elements cannot be added before this index when "more..." elements are loaded - private int myMaxFrozenIndex; - - public void setElementsComparator(Comparator elementsComparator) { - myElementsComparator = elementsComparator; - } - - public void setMaxFrozenIndex(int maxFrozenIndex) { - myMaxFrozenIndex = maxFrozenIndex; - } - - public boolean isResultsExpired() { - return resultsExpired; - } - - public void expireResults() { - resultsExpired = true; - } - - @Override - public int getSize() { - return listElements.size(); - } - - @Override - public Object getElementAt(int index) { - return listElements.get(index).getElement(); - } - - public List getItems() { - return new ArrayList<>(values()); - } - - public Collection getFoundItems(SearchEverywhereContributor contributor) { - return listElements.stream() - .filter(info -> info.getContributor() == contributor && info.getElement() != MORE_ELEMENT) - .map(info -> info.getElement()) - .collect(Collectors.toList()); - } - - public boolean hasMoreElements(SearchEverywhereContributor contributor) { - return Boolean.TRUE.equals(hasMoreContributors.get(contributor)); - } - - public void addElements(List items) { - if (items.isEmpty()) { - return; - } - - items = items.stream() - .sorted(myElementsComparator) - .collect(Collectors.toList()); - - if (resultsExpired) { - clearMoreItem(); - - Object[] oldItems = ArrayUtil.toObjectArray(getItems()); - Object[] newItems = items.stream() - .map(SearchEverywhereFoundElementInfo::getElement) - .toArray(); - try { - Diff.Change change = Diff.buildChanges(oldItems, newItems); - applyChange(change, items); - } - catch (FilesTooBigForDiffException e) { - LOG.error("Cannot calculate diff for updated search results"); - } - - resultsExpired = false; - } - else { - int startIndex = listElements.size(); - listElements.addAll(items); - int endIndex = listElements.size() - 1; - fireIntervalAdded(this, startIndex, endIndex); - - // there were items for this contributor before update - if (startIndex > 0) { - List lst = myMaxFrozenIndex >= 0 - ? listElements.subList(myMaxFrozenIndex + 1, listElements.size()) - : listElements; - lst.sort(myElementsComparator); - fireContentsChanged(this, 0, endIndex); - } - } - } - - public void clearMoreItem() { - if (listElements.isEmpty()) return; - - int lastItemIndex = listElements.size() - 1; - SearchEverywhereFoundElementInfo lastItem = listElements.get(lastItemIndex); - if (lastItem.getElement() == MORE_ELEMENT) { - listElements.remove(lastItemIndex); - fireIntervalRemoved(this, lastItemIndex, lastItemIndex); - } - } - - private void applyChange(Diff.Change change, List newItems) { - for (Diff.Change ch : toRevertedList(change)) { - if (ch.deleted > 0) { - listElements.subList(ch.line0, ch.line0 + ch.deleted).clear(); - fireIntervalRemoved(this, ch.line0, ch.line0 + ch.deleted - 1); - } - - if (ch.inserted > 0) { - List addedItems = newItems.subList(ch.line1, ch.line1 + ch.inserted); - listElements.addAll(ch.line0, addedItems); - fireIntervalAdded(this, ch.line0, ch.line0 + ch.inserted - 1); - } - } - } - - private static List toRevertedList(Diff.Change change) { - List res = new ArrayList<>(); - while (change != null) { - res.add(0, change); - change = change.link; - } - return res; - } - - public void removeElement(@NotNull Object item, SearchEverywhereContributor contributor) { - if (listElements.isEmpty()) return; - - for (int i = 0; i < listElements.size(); i++){ - SearchEverywhereFoundElementInfo info = listElements.get(i); - if (info.getContributor() == contributor && info.getElement().equals(item)) { - listElements.remove(i); - fireIntervalRemoved(this, i, i); - return; - } - } - } - - public void setHasMore(SearchEverywhereContributor contributor, boolean contributorHasMore) { - hasMoreContributors.put(contributor, contributorHasMore); - - int lasItemIndex = listElements.size() - 1; - if (lasItemIndex < 0) { - return; - } - - boolean hasMore = hasMoreContributors.values().stream().anyMatch(val -> val); - boolean alreadyHas = isMoreElement(lasItemIndex); - if (alreadyHas && !hasMore) { - listElements.remove(lasItemIndex); - fireIntervalRemoved(this, lasItemIndex, lasItemIndex); - } - - if (!alreadyHas && hasMore) { - listElements.add(new SearchEverywhereFoundElementInfo(MORE_ELEMENT, 0, null)); - lasItemIndex += 1; - fireIntervalAdded(this, lasItemIndex, lasItemIndex); - } - } - - public void clear() { - int index = listElements.size() - 1; - listElements.clear(); - hasMoreContributors.clear(); - if (index >= 0) { - fireIntervalRemoved(this, 0, index); - } - } - - public boolean contains(Object val) { - return values().contains(val); - } - - public boolean isMoreElement(int index) { - return listElements.get(index).getElement() == MORE_ELEMENT; - } - - public SearchEverywhereContributor getContributorForIndex(int index) { - //noinspection unchecked - return (SearchEverywhereContributor)listElements.get(index).getContributor(); - } - - public Map, Collection> getFoundElementsMap() { - return listElements.stream() - .filter(info -> info.element != MORE_ELEMENT) - .collect(Collectors.groupingBy(o -> o.getContributor(), Collectors.toCollection(ArrayList::new))); - } - - @NotNull - private List values() { - return Lists.transform(listElements, info -> info.getElement()); - } - } - - private class ShowInFindToolWindowAction extends DumbAwareAction { - - ShowInFindToolWindowAction() { - super(IdeBundle.messagePointer("show.in.find.window.button.name"), - IdeBundle.messagePointer("show.in.find.window.button.description"), AllIcons.General.Pin_tab); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - stopSearching(); - - Collection> contributors = myHeader.getSelectedTab().getContributors(); - contributors = ContainerUtil.filter(contributors, SearchEverywhereContributor::showInFindResults); - - if (contributors.isEmpty()) { - return; - } - - String searchText = getSearchPattern(); - String contributorsString = contributors.stream() - .map(SearchEverywhereContributor::getGroupName) - .collect(Collectors.joining(", ")); - - UsageViewPresentation presentation = new UsageViewPresentation(); - String tabCaptionText = IdeBundle.message("searcheverywhere.found.matches.title", searchText, contributorsString); - presentation.setCodeUsagesString(tabCaptionText); - presentation.setUsagesInGeneratedCodeString( - IdeBundle.message("searcheverywhere.found.matches.generated.code.title", searchText, contributorsString)); - presentation.setTargetsNodeText(IdeBundle.message("searcheverywhere.found.targets.title", searchText, contributorsString)); - presentation.setTabName(tabCaptionText); - presentation.setTabText(tabCaptionText); - - Collection usages = new LinkedHashSet<>(); - Collection targets = new LinkedHashSet<>(); - - Collection cached = contributors.stream() - .flatMap(contributor -> myListModel.getFoundItems(contributor).stream()) - .collect(Collectors.toSet()); - fillUsages(cached, usages, targets); - - Collection> contributorsForAdditionalSearch; - contributorsForAdditionalSearch = ContainerUtil.filter(contributors, contributor -> myListModel.hasMoreElements(contributor)); - - if (!contributorsForAdditionalSearch.isEmpty()) { - ProgressManager.getInstance().run(new Task.Modal(myProject, tabCaptionText, true) { - private final ProgressIndicator progressIndicator = new ProgressIndicatorBase(); - - @Override - public void run(@NotNull ProgressIndicator indicator) { - progressIndicator.start(); - TooManyUsagesStatus tooManyUsagesStatus = TooManyUsagesStatus.createFor(progressIndicator); - - Collection foundElements = new ArrayList<>(); - int alreadyFoundCount = cached.size(); - for (SearchEverywhereContributor contributor : contributorsForAdditionalSearch) { - if (progressIndicator.isCanceled()) break; - try { - fetch(contributor, foundElements, alreadyFoundCount, tooManyUsagesStatus); - } - catch (ProcessCanceledException ignore) { - } - } - fillUsages(foundElements, usages, targets); - } - - void fetch(SearchEverywhereContributor contributor, - Collection foundElements, - int alreadyFoundCount, - TooManyUsagesStatus tooManyUsagesStatus) { - contributor.fetchElements(searchText, progressIndicator, o -> { - if (progressIndicator.isCanceled()) { - return false; - } - - if (cached.contains(o)) { - return true; - } - - foundElements.add(o); - tooManyUsagesStatus.pauseProcessingIfTooManyUsages(); - if (foundElements.size() + alreadyFoundCount >= UsageLimitUtil.USAGES_LIMIT && - tooManyUsagesStatus.switchTooManyUsagesStatus()) { - UsageViewManagerImpl.showTooManyUsagesWarningLater(getProject(), tooManyUsagesStatus, progressIndicator, null); - return !progressIndicator.isCanceled(); - } - return true; - }); - } - - @Override - public void onCancel() { - progressIndicator.cancel(); - } - - @Override - public void onSuccess() { - showInFindWindow(targets, usages, presentation); - } - - @Override - public void onThrowable(@NotNull Throwable error) { - super.onThrowable(error); - progressIndicator.cancel(); - } - }); - } - else { - showInFindWindow(targets, usages, presentation); - } - closePopup(); - } - - private void fillUsages(Collection foundElements, Collection usages, Collection targets) { - ReadAction.run(() -> foundElements.stream() - .filter(o -> o instanceof PsiElement) - .forEach(o -> { - PsiElement element = (PsiElement)o; - if (element.getTextRange() != null) { - UsageInfo usageInfo = new UsageInfo(element); - usages.add(new UsageInfo2UsageAdapter(usageInfo)); - } - else { - targets.add(element); - } - })); - } - - private void showInFindWindow(Collection targets, Collection usages, UsageViewPresentation presentation) { - UsageTarget[] targetsArray = targets.isEmpty() ? UsageTarget.EMPTY_ARRAY - : PsiElement2UsageTargetAdapter.convert(PsiUtilCore.toPsiElementArray(targets)); - Usage[] usagesArray = usages.toArray(Usage.EMPTY_ARRAY); - UsageViewManager.getInstance(myProject).showUsages(targetsArray, usagesArray, presentation); - } - - @Override - public void update(@NotNull AnActionEvent e) { - if (myProject == null) { - e.getPresentation().setEnabled(false); - return; - } - - SETab selectedTab = myHeader != null ? myHeader.getSelectedTab() : null; - boolean enabled = selectedTab == null || selectedTab.getContributors().stream().anyMatch(c -> c.showInFindResults()); - e.getPresentation().setEnabled(enabled); - e.getPresentation().setIcon(ToolWindowManager.getInstance(myProject).getLocationIcon(ToolWindowId.FIND, AllIcons.General.Pin_tab)); - } - } - - private class CompleteCommandAction extends DumbAwareAction { - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - if (completeCommand()) { - FeatureUsageData data = SearchEverywhereUsageTriggerCollector - .createData(null) - .addInputEvent(e); - featureTriggered(SearchEverywhereUsageTriggerCollector.COMMAND_COMPLETED, data); - } - } - - @Override - public void update(@NotNull AnActionEvent e) { - e.getPresentation().setEnabled(getCompleteCommand().isPresent()); - } - - private boolean completeCommand() { - Optional suggestedCommand = getCompleteCommand(); - if (suggestedCommand.isPresent()) { - mySearchField.setText(suggestedCommand.get().getCommandWithPrefix() + " "); - return true; - } - - return false; - } - - private Optional getCompleteCommand() { - String pattern = getSearchPattern(); - String commandPrefix = SearchTopHitProvider.getTopHitAccelerator(); - if (pattern.startsWith(commandPrefix) && !pattern.contains(" ")) { - String typedCommand = pattern.substring(commandPrefix.length()); - SearchEverywhereCommandInfo command = getSelectedCommand(typedCommand).orElseGet(() -> { - List completions = getCommandsForCompletion(myHeader.getSelectedTab().getContributors(), typedCommand); - return completions.isEmpty() ? null : completions.get(0); - }); - - return Optional.ofNullable(command); - } - - return Optional.empty(); - } - } - - @Nls(capitalization = Nls.Capitalization.Sentence) - private String getNotFoundText() { - SETab selectedTab = myHeader.getSelectedTab(); - if (!selectedTab.isSingleContributor()) return IdeBundle.message("searcheverywhere.nothing.found.for.all.anywhere"); - - String groupName = selectedTab.getContributors().get(0).getFullGroupName(); - return IdeBundle.message("searcheverywhere.nothing.found.for.contributor.anywhere", groupName.toLowerCase(Locale.ROOT)); - } - - private void featureTriggered(@NotNull String featureID, @Nullable FeatureUsageData data) { - if (data != null) { - SearchEverywhereUsageTriggerCollector.trigger(myProject, featureID, data); - } - else { - SearchEverywhereUsageTriggerCollector.trigger(myProject, featureID); - } - } - - private final SearchListener mySearchListener = new SearchListener(); - - private class SearchListener implements SESearcher.Listener { - private Consumer> testCallback; - - @Override - public void elementsAdded(@NotNull List list) { - boolean wasEmpty = myListModel.listElements.isEmpty(); - - mySelectionTracker.lock(); - myListModel.addElements(list); - mySelectionTracker.unlock(); - - mySelectionTracker.restoreSelection(); - - if (wasEmpty && !myListModel.listElements.isEmpty()) { - Object prevSelection = ((SearchEverywhereManagerImpl)SearchEverywhereManager.getInstance(myProject)) - .getPrevSelection(getSelectedTabID()); - if (prevSelection instanceof Integer) { - for (SearchEverywhereFoundElementInfo info : myListModel.listElements) { - if (Objects.hashCode(info.element) == ((Integer)prevSelection).intValue()) { - myResultsList.setSelectedValue(info.element, true); - break; - } - } - } - } - } - - @Override - public void elementsRemoved(@NotNull List list) { - list.forEach(info -> myListModel.removeElement(info.getElement(), info.getContributor())); - } - - @Override - public void searchFinished(@NotNull Map, Boolean> hasMoreContributors) { - String pattern = getSearchPattern(); - pattern = pattern.replaceAll("^" + SearchTopHitProvider.getTopHitAccelerator() + "\\S+\\s*", ""); - if (myResultsList.isEmpty() || myListModel.isResultsExpired()) { - if (myHeader.canSetEverywhere() && !myHeader.isEverywhere() && !pattern.isEmpty()) { - myHeader.autoSetEverywhere(true); - myNotFoundString = pattern; - return; - } - - hideHint(); - if (myListModel.isResultsExpired()) { - myListModel.clear(); - } - } - - myResultsList.setEmptyText(pattern.isEmpty() ? "" : getNotFoundText()); - hasMoreContributors.forEach(myListModel::setHasMore); - - myListModel.setMaxFrozenIndex(-1); - mySelectionTracker.resetSelectionIfNeeded(); - - if (testCallback != null) testCallback.consume(myListModel.getItems()); - } - - @TestOnly - void setTestCallback(@Nullable Consumer> callback) { - testCallback = callback; - } - } - - private final SearchEverywhereContributor myStubCommandContributor = new SearchEverywhereContributor() { - @NotNull - @Override - public String getSearchProviderId() { - return "CommandsContributor"; - } - - @NotNull - @Override - public String getGroupName() { - return IdeBundle.message("searcheverywhere.commands.tab.name"); - } - - @Override - public int getSortWeight() { - return 10; - } - - @Override - public boolean showInFindResults() { - return false; - } - - @Override - public void fetchElements(@NotNull String pattern, - @NotNull ProgressIndicator progressIndicator, - @NotNull Processor consumer) {} - - @Override - public boolean processSelectedItem(@NotNull Object selected, int modifiers, @NotNull String searchText) { - mySearchField.setText(((SearchEverywhereCommandInfo)selected).getCommandWithPrefix() + " "); - featureTriggered(SearchEverywhereUsageTriggerCollector.COMMAND_COMPLETED, null); - return false; - } - - @NotNull - @Override - public ListCellRenderer getElementsRenderer() { - return myCommandRenderer; - } - - @Nullable - @Override - public Object getDataForItem(@NotNull Object element, @NotNull String dataId) { - return null; - } - }; -} diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/ThrottlingListenerWrapper.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/ThrottlingListenerWrapper.java deleted file mode 100644 index f20feacdf360..000000000000 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/ThrottlingListenerWrapper.java +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.ide.actions.searcheverywhere.mixed; - -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereContributor; -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereFoundElementInfo; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.util.Alarm; -import org.jetbrains.annotations.NotNull; - -import java.util.*; -import java.util.concurrent.Executor; -import java.util.function.BiConsumer; - -/** - * Implementation of {@link MultiThreadSearcher.Listener} which decrease events rate and raise batch updates - * each {@code throttlingDelay} milliseconds. - *
- * Not thread-safe and should be notified only in EDT - */ -class ThrottlingListenerWrapper implements SESearcher.Listener { - - public final int myThrottlingDelay; - - private final SESearcher.Listener myDelegateListener; - private final Executor myDelegateExecutor; - - private final Buffer myBuffer = new Buffer(); - private final BiConsumer, List> myFlushConsumer; - - private final Alarm flushAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); - private boolean flushScheduled; - - ThrottlingListenerWrapper(int throttlingDelay, SESearcher.Listener delegateListener, Executor delegateExecutor) { - myThrottlingDelay = throttlingDelay; - myDelegateListener = delegateListener; - myDelegateExecutor = delegateExecutor; - - myFlushConsumer = (added, removed) -> { - if (!added.isEmpty()) { - myDelegateExecutor.execute(() -> myDelegateListener.elementsAdded(added)); - } - if (!removed.isEmpty()) { - myDelegateExecutor.execute(() -> myDelegateListener.elementsRemoved(removed)); - } - }; - } - - public void clearBuffer() { - ApplicationManager.getApplication().assertIsDispatchThread(); - myBuffer.clear(); - cancelScheduledFlush(); - } - - @Override - public void elementsAdded(@NotNull List list) { - ApplicationManager.getApplication().assertIsDispatchThread(); - myBuffer.addEvent(new Event(Event.ADD, list)); - scheduleFlushBuffer(); - } - - @Override - public void elementsRemoved(@NotNull List list) { - ApplicationManager.getApplication().assertIsDispatchThread(); - myBuffer.addEvent(new Event(Event.REMOVE, list)); - scheduleFlushBuffer(); - } - - @Override - public void searchFinished(@NotNull Map, Boolean> hasMoreContributors) { - ApplicationManager.getApplication().assertIsDispatchThread(); - myBuffer.flush(myFlushConsumer); - myDelegateExecutor.execute(() -> myDelegateListener.searchFinished(hasMoreContributors)); - cancelScheduledFlush(); - } - - private void scheduleFlushBuffer() { - ApplicationManager.getApplication().assertIsDispatchThread(); - - Runnable flushTask = () -> { - ApplicationManager.getApplication().assertIsDispatchThread(); - if (!flushScheduled) return; - flushScheduled = false; - myBuffer.flush(myFlushConsumer); - }; - - if (!flushScheduled) { - flushAlarm.addRequest(flushTask, myThrottlingDelay); - flushScheduled = true; - } - } - - private void cancelScheduledFlush() { - ApplicationManager.getApplication().assertIsDispatchThread(); - flushAlarm.cancelAllRequests(); - flushScheduled = false; - } - - private static class Event { - static final int REMOVE = 0; - static final int ADD = 1; - - final int type; - final List items; - - Event(int type, List items) { - this.type = type; - this.items = items; - } - } - - private static class Buffer { - private final Queue myQueue = new ArrayDeque<>(); - - public void addEvent(Event event) { - myQueue.add(event); - } - - public void flush(BiConsumer, ? super List> consumer) { - List added = new ArrayList<>(); - List removed = new ArrayList<>(); - myQueue.forEach(event -> { - if (event.type == Event.ADD) { - added.addAll(event.items); - } else { - event.items.forEach(removing -> { - if (!added.removeIf(existing -> existing.getContributor() == removing.getContributor() && existing.getElement() == removing.getElement())) { - removed.add(removing); - } - }); - } - }); - myQueue.clear(); - consumer.accept(added, removed); - } - - public void clear() { - myQueue.clear(); - } - } - - -} diff --git a/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MixingMultiThreadSearchTest.groovy b/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MixingMultiThreadSearchTest.groovy index e87427f049a3..fd1b65beb879 100644 --- a/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MixingMultiThreadSearchTest.groovy +++ b/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MixingMultiThreadSearchTest.groovy @@ -164,7 +164,7 @@ class MixingMultiThreadSearchTest extends BasePlatformTestCase { private void testScenario(Scenario scenario) { SearchResultsCollector collector = new SearchResultsCollector() Alarm alarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, getTestRootDisposable()) - MultiThreadSearcher searcher = new MultiThreadSearcher(collector, { command -> alarm.addRequest(command, 0) }, ourEqualityProviders) + SESearcher searcher = new MixedResultsSearcher(collector, { command -> alarm.addRequest(command, 0) }, ourEqualityProviders) ProgressIndicator indicator = searcher.search(scenario.contributorsAndLimits, "tst") try { diff --git a/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MultiThreadSearchDeadlockTest.java b/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MultiThreadSearchDeadlockTest.java index 99cc0b1d06f5..3a75e60424f6 100644 --- a/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MultiThreadSearchDeadlockTest.java +++ b/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MultiThreadSearchDeadlockTest.java @@ -45,7 +45,7 @@ public class MultiThreadSearchDeadlockTest extends BasePlatformTestCase { Collector collector = new Collector(); Alarm alarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, getTestRootDisposable()); - MultiThreadSearcher searcher = new MultiThreadSearcher(collector, command -> alarm.addRequest(command, 0), ourEqualityProviders); + SESearcher searcher = new GroupedResultsSearcher(collector, command -> alarm.addRequest(command, 0), ourEqualityProviders); ProgressIndicator progressIndicator = searcher.search(contributorsMap, "tst"); try { @@ -73,7 +73,7 @@ public class MultiThreadSearchDeadlockTest extends BasePlatformTestCase { Collector collector = new Collector(); Alarm alarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, getTestRootDisposable()); - MultiThreadSearcher searcher = new MultiThreadSearcher(collector, command -> alarm.addRequest(command, 0), ourEqualityProviders); + SESearcher searcher = new GroupedResultsSearcher(collector, command -> alarm.addRequest(command, 0), ourEqualityProviders); ProgressIndicator progressIndicator = searcher.search(contributorsMap, "tst"); try { @@ -105,7 +105,7 @@ public class MultiThreadSearchDeadlockTest extends BasePlatformTestCase { Collector collector = new Collector(); Alarm alarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, getTestRootDisposable()); - MultiThreadSearcher searcher = new MultiThreadSearcher(collector, command -> alarm.addRequest(command, 0), ourEqualityProviders); + SESearcher searcher = new GroupedResultsSearcher(collector, command -> alarm.addRequest(command, 0), ourEqualityProviders); ProgressIndicator progressIndicator = searcher.search(contributorsMap, "tst"); try { diff --git a/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MultiThreadSearchTest.java b/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MultiThreadSearchTest.java index 58baab160372..24f180ef1b6c 100644 --- a/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MultiThreadSearchTest.java +++ b/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MultiThreadSearchTest.java @@ -30,7 +30,7 @@ public class MultiThreadSearchTest extends BasePlatformTestCase { Collection scenarios = createMultithreadScenarios(); SearchResultsCollector collector = new SearchResultsCollector(); Alarm alarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, getTestRootDisposable()); - MultiThreadSearcher searcher = new MultiThreadSearcher(collector, command -> alarm.addRequest(command, 0), ourEqualityProviders); + SESearcher searcher = new GroupedResultsSearcher(collector, command -> alarm.addRequest(command, 0), ourEqualityProviders); scenarios.forEach(scenario -> { ProgressIndicator indicator = searcher.search(scenario.contributorsAndLimits, "tst"); @@ -262,7 +262,7 @@ public class MultiThreadSearchTest extends BasePlatformTestCase { } } - private static class SearchResultsCollector implements MultiThreadSearcher.Listener { + private static class SearchResultsCollector implements SESearcher.Listener { private final Map> myMap = new ConcurrentHashMap<>(); private final AtomicBoolean myFinished = new AtomicBoolean(false); diff --git a/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/SearchModelTest.java b/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/SearchModelTest.java index 618cac61ce0f..9a9df971a86c 100644 --- a/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/SearchModelTest.java +++ b/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/SearchModelTest.java @@ -13,8 +13,6 @@ import java.util.Arrays; import java.util.List; import java.util.UUID; -import static com.intellij.ide.actions.searcheverywhere.SearchEverywhereUI.SearchListModel; - public class SearchModelTest extends BasePlatformTestCase { private final static SearchEverywhereContributor STUB_CONTRIBUTOR_1 = createStubContributor(100); @@ -22,7 +20,7 @@ public class SearchModelTest extends BasePlatformTestCase { private final static SearchEverywhereContributor STUB_CONTRIBUTOR_3 = createStubContributor(300); public void testElementsAdding() { - SearchListModel model = new SearchListModel(); + SearchListModel model = new GroupedSearchListModel(); // adding to empty ----------------------------------------------------------------------- model.addElements(Arrays.asList( @@ -67,7 +65,7 @@ public class SearchModelTest extends BasePlatformTestCase { actualItems = model.getItems(); expectedItems = Arrays.asList("item_1_10", "item_1_20", "item_1_25", "item_1_30", "item_1_35", SearchListModel.MORE_ELEMENT, - "item_2_05", "item_2_10", "item_2_20","item_2_23", "item_2_25","item_2_30", "item_2_40", + "item_2_05", "item_2_10", "item_2_20", "item_2_23", "item_2_25", "item_2_30", "item_2_40", "item_3_03", "item_3_05", "item_3_10", "item_3_20", "item_3_30", "item_3_40", "item_3_50", SearchListModel.MORE_ELEMENT); Assert.assertEquals(expectedItems, actualItems);