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 f40755d72af9..f6b9de593c72 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.SearchEverywhereUI +import com.intellij.ide.actions.searcheverywhere.SearchEverywhereUIBase 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 SearchEverywhereUI } as SearchEverywhereUI + return GuiRobotHolder.robot.finder().find { it is SearchEverywhereUIBase } as SearchEverywhereUIBase } } \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/ide/JavaClassAndFileEqualityProvider.java b/java/java-impl/src/com/intellij/ide/JavaClassAndFileEqualityProvider.java index fecb200fcc05..6cb0f17e5f37 100644 --- a/java/java-impl/src/com/intellij/ide/JavaClassAndFileEqualityProvider.java +++ b/java/java-impl/src/com/intellij/ide/JavaClassAndFileEqualityProvider.java @@ -21,7 +21,9 @@ public class JavaClassAndFileEqualityProvider implements SEResultsEqualityProvid } if (isClassAndFile(newItemInfo, alreadyFoundItemInfo) && isSameFile(newElementPsi, alreadyFoundPsi)) { - return newItemInfo.priority > alreadyFoundItemInfo.priority ? SEEqualElementsActionType.REPLACE : SEEqualElementsActionType.SKIP; + return SearchEverywhereFoundElementInfo.COMPARATOR.compare(newItemInfo, alreadyFoundItemInfo) > 0 + ? SEEqualElementsActionType.REPLACE + : SEEqualElementsActionType.SKIP; } return SEEqualElementsActionType.DO_NOTHING; 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 52dff1cef460..ef9793fde73b 100644 --- a/java/java-tests/testSrc/com/intellij/java/navigation/SearchEverywhereTest.groovy +++ b/java/java-tests/testSrc/com/intellij/java/navigation/SearchEverywhereTest.groovy @@ -1,16 +1,23 @@ // 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.java.navigation -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereContributor -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereManagerImpl -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereUI +import com.intellij.ide.actions.searcheverywhere.* +import com.intellij.ide.actions.searcheverywhere.mixed.SearchEverywhereUIMixedResults +import com.intellij.openapi.application.Experiments +import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.util.Disposer import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase +import com.intellij.util.Processor +import org.jetbrains.annotations.NotNull + +import javax.swing.* import static com.intellij.testFramework.PlatformTestUtil.waitForFuture class SearchEverywhereTest extends LightJavaCodeInsightFixtureTestCase { - SearchEverywhereUI mySearchUI + static final int SEARCH_TIMEOUT = 5000 + + SearchEverywhereUIBase mySearchUI @Override protected void tearDown() throws Exception { @@ -29,17 +36,127 @@ class SearchEverywhereTest extends LightJavaCodeInsightFixtureTestCase { def ui = createTestUI([ChooseByNameTest.createClassContributor(project)]) def future = ui.findElementsForPattern("StrBuffer") - assert waitForFuture(future, 5000) == [strBuffer] + assert waitForFuture(future, SEARCH_TIMEOUT) == [strBuffer] future = ui.findElementsForPattern("StringBuffer") - assert waitForFuture(future, 5000) == [stringBuffer] + assert waitForFuture(future, SEARCH_TIMEOUT) == [stringBuffer] } - private SearchEverywhereUI createTestUI(List> contributors) { + void "test mixing classes and files"() { + withMixingEnabled({ + def testClass = myFixture.addClass("class TestClass{}") + def anotherTestClass = myFixture.addClass("class AnotherTestClass{}") + def testFile = myFixture.addFileToProject("testClass.txt", "") + + def ui = createTestUI([ + ChooseByNameTest.createClassContributor(project), + ChooseByNameTest.createFileContributor(project) + ]) + + def future = ui.findElementsForPattern("TestClass") + assert waitForFuture(future, SEARCH_TIMEOUT) == [testClass, testFile, anotherTestClass] + + future = ui.findElementsForPattern("testClass") + assert waitForFuture(future, SEARCH_TIMEOUT) == [testFile, testClass, anotherTestClass] + + def testClass2 = myFixture.addClass("class testClass2{}") + + future = ui.findElementsForPattern("testClass") + assert waitForFuture(future, SEARCH_TIMEOUT) == [testClass2, testFile, testClass, anotherTestClass] + }) + } + + void "test mixing results from stub contributors"() { + withMixingEnabled({ + def contributor1 = new StubContributor("contributor1", 1) + def contributor2 = new StubContributor("contributor2", 2) + def contributor3 = new StubContributor("contributor3", 3) + + contributor1.addElements(["item1": 10, "item2": 12, "item3": 8]) + contributor2.addElements(["item3": 8, "item4": 10, "item5": 11, "item8": 15]) + contributor3.addElements(["item3": 8, "item5": 11, "item6": 10, "item7": 15]) + + def ui = createTestUI([contributor2, contributor1, contributor3]) + def future = ui.findElementsForPattern("ignored") + assert waitForFuture(future, SEARCH_TIMEOUT) == ["item8", "item7", "item2", "item5", "item1", "item4", "item6", "item3"] + }) + } + + private SearchEverywhereUIBase createTestUI(List> contributors) { if (mySearchUI != null) Disposer.dispose(mySearchUI) - mySearchUI = new SearchEverywhereUI(project, contributors) + def mixingEnabled = Experiments.getInstance().isFeatureEnabled("search.everywhere.mixed.results") + mySearchUI = mixingEnabled ? new SearchEverywhereUIMixedResults(project, contributors) + : new SearchEverywhereUI(project, contributors) mySearchUI.switchToContributor(SearchEverywhereManagerImpl.ALL_CONTRIBUTORS_GROUP_ID) return mySearchUI } + + private static void withMixingEnabled(Runnable task) { + Experiments.getInstance().setFeatureEnabled("search.everywhere.mixed.results", true) + try { + task.run() + } + finally { + Experiments.getInstance().setFeatureEnabled("search.everywhere.mixed.results", false) + } + } + + private static class StubContributor implements WeightedSearchEverywhereContributor { + + private final String myId + private final int mySortWeight + private final Map myElements = new HashMap<>() + + StubContributor(String id, int sortWeight) { + myId = id + mySortWeight = sortWeight + } + + void addElements(Map elements) { + myElements.putAll(elements) + } + + @Override + void fetchWeightedElements(@NotNull String pattern, + @NotNull ProgressIndicator progressIndicator, + @NotNull Processor> consumer) { + myElements.forEach({element, weight -> consumer.process(new FoundItemDescriptor(element, weight))}) + } + + @Override + String getSearchProviderId() { + return myId + } + + @Override + String getGroupName() { + return myId + } + + @Override + int getSortWeight() { + return mySortWeight + } + + @Override + boolean showInFindResults() { + return false + } + + @Override + boolean processSelectedItem(@NotNull Object selected, int modifiers, @NotNull String searchText) { + return false + } + + @Override + ListCellRenderer getElementsRenderer() { + return new DefaultListCellRenderer() + } + + @Override + Object getDataForItem(@NotNull Object element, @NotNull String dataId) { + return null + } + } } diff --git a/platform/lang-impl/src/com/intellij/ide/actions/SearchEverywhereAction.java b/platform/lang-impl/src/com/intellij/ide/actions/SearchEverywhereAction.java index 5d31d85dd9b7..b316c4983796 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/SearchEverywhereAction.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/SearchEverywhereAction.java @@ -16,7 +16,7 @@ import com.intellij.icons.AllIcons; import com.intellij.ide.*; import com.intellij.ide.actions.searcheverywhere.SearchEverywhereManager; import com.intellij.ide.actions.searcheverywhere.SearchEverywhereManagerImpl; -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereUI; +import com.intellij.ide.actions.searcheverywhere.SearchEverywhereUIBase; import com.intellij.ide.actions.searcheverywhere.statistics.SearchEverywhereUsageTriggerCollector; import com.intellij.ide.lightEdit.LightEdit; import com.intellij.ide.structureView.StructureView; @@ -983,7 +983,7 @@ public class SearchEverywhereAction extends AnAction implements CustomComponentA super(false, "SearchEveryWhereHistory"); JTextField editor = getTextEditor(); editor.setOpaque(false); - editor.putClientProperty(SearchEverywhereUI.SEARCH_EVERYWHERE_SEARCH_FILED_KEY, true); + editor.putClientProperty(SearchEverywhereUIBase.SEARCH_EVERYWHERE_SEARCH_FILED_KEY, true); editor.putClientProperty("JTextField.Search.noBorderRing", Boolean.TRUE); if (StartupUiUtil.isUnderDarcula()) { 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 dd22b093a9af..eba8295d8a22 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 @@ -219,7 +219,7 @@ public abstract class AbstractGotoSEContributor implements WeightedSearchEverywh } }); } - result.add(new SearchEverywhereUI.FiltersAction(filter, onChanged)); + result.add(new SearchEverywhereUIBase.FiltersAction(filter, onChanged)); return result; } diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/PsiElementsEqualityProvider.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/PsiElementsEqualityProvider.java index dc2506f85b06..79228b29e8df 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/PsiElementsEqualityProvider.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/PsiElementsEqualityProvider.java @@ -21,7 +21,9 @@ public class PsiElementsEqualityProvider implements SEResultsEqualityProvider { } if (Objects.equals(newElementPsi, alreadyFoundPsi)) { - return newItemInfo.priority > alreadyFoundItemInfo.priority ? SEEqualElementsActionType.REPLACE : SEEqualElementsActionType.SKIP; + return SearchEverywhereFoundElementInfo.COMPARATOR.compare(newItemInfo, alreadyFoundItemInfo) > 0 + ? SEEqualElementsActionType.REPLACE + : SEEqualElementsActionType.SKIP; } return SEEqualElementsActionType.DO_NOTHING; diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereFoundElementInfo.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereFoundElementInfo.java index 5ad3ec3906ae..da7cb07159c5 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereFoundElementInfo.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereFoundElementInfo.java @@ -1,6 +1,8 @@ // Copyright 2000-2019 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 java.util.Comparator; + /** * Class containing info about found elements */ @@ -26,4 +28,11 @@ public class SearchEverywhereFoundElementInfo { public SearchEverywhereContributor getContributor() { return contributor; } + + public static final Comparator COMPARATOR = (o1, o2) -> { + int res = Integer.compare(o1.priority, o2.priority); + if (res != 0) return res; + + return -Integer.compare(o1.contributor.getSortWeight(), o2.contributor.getSortWeight()); + }; } 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 5cb9e1c64808..66b26e187907 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 @@ -2,12 +2,15 @@ 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.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.KeyboardShortcut; import com.intellij.openapi.actionSystem.PlatformDataKeys; 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; @@ -47,7 +50,7 @@ public class SearchEverywhereManagerImpl implements SearchEverywhereManager { private final Project myProject; private JBPopup myBalloon; - private SearchEverywhereUI mySearchEverywhereUI; + private SearchEverywhereUIBase mySearchEverywhereUI; private Dimension myBalloonFullSize; private final SearchHistoryList myHistoryList = new SearchHistoryList(); @@ -144,7 +147,7 @@ public class SearchEverywhereManagerImpl implements SearchEverywhereManager { myBalloonFullSize = null; }); - if (mySearchEverywhereUI.getViewType() == SearchEverywhereUI.ViewType.SHORT) { + if (mySearchEverywhereUI.getViewType() == BigPopupUI.ViewType.SHORT) { myBalloonFullSize = WindowStateService.getInstance(myProject).getSize(LOCATION_SETTINGS_KEY); Dimension prefSize = mySearchEverywhereUI.getPreferredSize(); myBalloon.setSize(prefSize); @@ -156,7 +159,7 @@ public class SearchEverywhereManagerImpl implements SearchEverywhereManager { Point savedLocation = WindowStateService.getInstance(myProject).getLocation(LOCATION_SETTINGS_KEY); //for first show and short mode popup should be shifted to the top screen half - if (savedLocation == null && mySearchEverywhereUI.getViewType() == SearchEverywhereUI.ViewType.SHORT) { + if (savedLocation == null && mySearchEverywhereUI.getViewType() == BigPopupUI.ViewType.SHORT) { Window window = project != null ? WindowManager.getInstance().suggestParentWindow(project) : KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow(); @@ -226,9 +229,11 @@ public class SearchEverywhereManagerImpl implements SearchEverywhereManager { myEverywhere = everywhere; } - private SearchEverywhereUI createView(Project project, + private SearchEverywhereUIBase createView(Project project, List> contributors) { - SearchEverywhereUI view = new SearchEverywhereUI(project, contributors, myTabsShortcutsMap::get); + SearchEverywhereUIBase view = Experiments.getInstance().isFeatureEnabled("search.everywhere.mixed.results") + ? new SearchEverywhereUIMixedResults(project, contributors, myTabsShortcutsMap::get) + : new SearchEverywhereUI(project, contributors, myTabsShortcutsMap::get); view.setSearchFinishedHandler(() -> { if (isShown()) { @@ -248,7 +253,7 @@ public class SearchEverywhereManagerImpl implements SearchEverywhereManager { JBInsets.addTo(minSize, myBalloon.getContent().getInsets()); myBalloon.setMinimumSize(minSize); - if (viewType == SearchEverywhereUI.ViewType.SHORT) { + if (viewType == BigPopupUI.ViewType.SHORT) { myBalloonFullSize = myBalloon.getSize(); JBInsets.removeFrom(myBalloonFullSize, myBalloon.getContent().getInsets()); myBalloon.pack(false, true); @@ -294,12 +299,12 @@ public class SearchEverywhereManagerImpl implements SearchEverywhereManager { } @Nullable - Object getPrevSelection(String contributorID) { + public Object getPrevSelection(String contributorID) { return myPrevSelections.remove(contributorID); } private void saveSize() { - if (mySearchEverywhereUI.getViewType() == SearchEverywhereUI.ViewType.SHORT) { + if (mySearchEverywhereUI.getViewType() == BigPopupUI.ViewType.SHORT) { WindowStateService.getInstance(myProject).putSize(LOCATION_SETTINGS_KEY, myBalloonFullSize); } } 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 d383a6738dfe..9f172bef4bdf 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 @@ -86,11 +86,9 @@ import java.util.stream.IntStream; * @author Konstantin Bulenkov * @author Mikhail.Sokolov */ -public final class SearchEverywhereUI extends BigPopupUI implements DataProvider, QuickSearchComponent { +public final class SearchEverywhereUI extends SearchEverywhereUIBase implements DataProvider, QuickSearchComponent { private static final Logger LOG = Logger.getInstance(SearchEverywhereUI.class); - 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; @@ -1483,55 +1481,6 @@ public final class SearchEverywhereUI extends BigPopupUI implements DataProvider } } - static class FiltersAction extends ShowFilterAction { - final PersistentSearchEverywhereContributorFilter filter; - final Runnable rebuildRunnable; - - 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; - } - } - private class CompleteCommandAction extends DumbAwareAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { 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 new file mode 100644 index 000000000000..dab127517deb --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/SearchEverywhereUIBase.java @@ -0,0 +1,75 @@ +// 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 javax.swing.*; + +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 switchToContributor(@NotNull String contributorID); + + public abstract String getSelectedContributorID(); + + @Nullable + public abstract Object getSelectionIdentity(); + + 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/ThrottlingListenerWrapper.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/ThrottlingListenerWrapper.java index 393c03002bb7..689b702f9ba3 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 @@ -19,7 +19,7 @@ class ThrottlingListenerWrapper implements MultiThreadSearcher.Listener { public final int myThrottlingDelay; - private final MultiThreadSearcher.Listener myDelegateListener; + private final SESearcher.Listener myDelegateListener; private final Executor myDelegateExecutor; private final Buffer myBuffer = new Buffer(); diff --git a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/TrivialElementsEqualityProvider.java b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/TrivialElementsEqualityProvider.java index 1af43878abab..00af9fcc08c8 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/TrivialElementsEqualityProvider.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/TrivialElementsEqualityProvider.java @@ -11,7 +11,9 @@ public class TrivialElementsEqualityProvider implements SEResultsEqualityProvide @Override public SEEqualElementsActionType compareItems(@NotNull SearchEverywhereFoundElementInfo newItem, @NotNull SearchEverywhereFoundElementInfo alreadyFoundItem) { if (Objects.equals(newItem.getElement(), alreadyFoundItem.getElement())) { - return newItem.getPriority() > alreadyFoundItem.getPriority() ? SEEqualElementsActionType.REPLACE : SEEqualElementsActionType.SKIP; + return SearchEverywhereFoundElementInfo.COMPARATOR.compare(newItem, alreadyFoundItem) > 0 + ? SEEqualElementsActionType.REPLACE + : SEEqualElementsActionType.SKIP; } return SEEqualElementsActionType.DO_NOTHING; } 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/mixed/MultiThreadSearcher.java new file mode 100644 index 000000000000..fef01120d25c --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/MultiThreadSearcher.java @@ -0,0 +1,405 @@ +// 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.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; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.util.ProgressIndicatorBase; +import com.intellij.util.ConcurrencyUtil; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.intellij.ide.actions.searcheverywhere.SEResultsEqualityProvider.SEEqualElementsActionType.REPLACE; +import static com.intellij.ide.actions.searcheverywhere.SEResultsEqualityProvider.SEEqualElementsActionType.SKIP; + +/** + * @author msokolov + */ +class MultiThreadSearcher implements SESearcher { + + private static final Logger LOG = Logger.getInstance(MultiThreadSearcher.class); + + @NotNull private final Listener myListener; + @NotNull private final Executor myNotificationExecutor; + @NotNull private final SEResultsEqualityProvider myEqualityProvider; + + /** + * Creates MultiThreadSearcher with search results {@link Listener} and specifies executor which going to be used to call listener methods. + * Use this constructor when you for example need to receive listener events only in AWT thread + * @param listener {@link Listener} to get notifications about searching process + * @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) { + myListener = listener; + myNotificationExecutor = notificationExecutor; + myEqualityProvider = SEResultsEqualityProvider.composite(equalityProviders); + } + + @Override + public ProgressIndicator search(@NotNull Map, Integer> contributorsAndLimits, + @NotNull String pattern) { + LOG.debug("Search started for pattern [", pattern, "]"); + + Collection> contributors = contributorsAndLimits.keySet(); + if (pattern.isEmpty()) { + if (ApplicationManager.getApplication().isUnitTestMode()) { + contributors = Collections.emptySet(); //empty search string is not allowed for tests + } + else { + contributors = ContainerUtil.filter(contributors, contributor -> contributor.isEmptyPatternSupported()); + } + } + + Function accumulatorSupplier = indicator -> + new ResultsAccumulator(contributorsAndLimits, myEqualityProvider, myListener, myNotificationExecutor, indicator); + + return performSearch(contributors, pattern, accumulatorSupplier); + } + + @Override + public ProgressIndicator findMoreItems(@NotNull Map, Collection> alreadyFound, + @NotNull Map, Integer> contributorsAndLimits, + @NotNull String pattern) { + Function accumulatorSupplier = indicator -> + new ResultsAccumulator(alreadyFound, contributorsAndLimits, myEqualityProvider, myListener, myNotificationExecutor, indicator); + + return performSearch(contributorsAndLimits.keySet(), pattern, accumulatorSupplier); + } + + @NotNull + private static ProgressIndicator performSearch(@NotNull Collection> contributors, + @NotNull String pattern, + @NotNull Function accumulatorSupplier) { + ProgressIndicator indicator; + ResultsAccumulator accumulator; + if (!contributors.isEmpty()) { + CountDownLatch latch = new CountDownLatch(contributors.size()); + ProgressIndicatorWithCancelListener indicatorWithCancelListener = new ProgressIndicatorWithCancelListener(); + accumulator = accumulatorSupplier.apply(indicatorWithCancelListener); + + for (SearchEverywhereContributor contributor : contributors) { + Runnable task = createSearchTask(pattern, accumulator, + indicatorWithCancelListener, contributor, () -> latch.countDown()); + ApplicationManager.getApplication().executeOnPooledThread(task); + } + + Runnable finisherTask = createFinisherTask(latch, accumulator, indicatorWithCancelListener); + Future finisherFeature = ApplicationManager.getApplication().executeOnPooledThread(finisherTask); + indicatorWithCancelListener.setCancelCallback(() -> { + accumulator.stop(); + finisherFeature.cancel(true); + }); + indicator = indicatorWithCancelListener; + } + else { + indicator = new ProgressIndicatorBase(); + accumulator = accumulatorSupplier.apply(indicator); + } + + indicator.start(); + if (contributors.isEmpty()) { + indicator.stop(); + accumulator.searchFinished(); + } + return indicator; + } + + @NotNull + private static Runnable createSearchTask(String pattern, + ResultsAccumulator accumulator, + ProgressIndicator indicator, + SearchEverywhereContributor contributor, + Runnable finalCallback) { + //noinspection unchecked + ContributorSearchTask task = new ContributorSearchTask<>( + (SearchEverywhereContributor)contributor, pattern, + accumulator, indicator, finalCallback); + return ConcurrencyUtil.underThreadNameRunnable("SE-SearchTask-" + contributor.getSearchProviderId(), task); + } + + private static Runnable createFinisherTask(CountDownLatch latch, ResultsAccumulator accumulator, ProgressIndicator indicator) { + return ConcurrencyUtil.underThreadNameRunnable("SE-FinisherTask", () -> { + try { + latch.await(); + if (!indicator.isCanceled()) { + accumulator.searchFinished(); + } + indicator.stop(); + } + catch (InterruptedException e) { + LOG.debug("Finisher interrupted before search process is finished"); + } + }); + } + + private static class ContributorSearchTask implements Runnable { + + private final ResultsAccumulator myAccumulator; + private final Runnable finishCallback; + + private final SearchEverywhereContributor myContributor; + private final String myPattern; + private final ProgressIndicator myIndicator; + + private ContributorSearchTask(SearchEverywhereContributor contributor, + String pattern, + ResultsAccumulator accumulator, + ProgressIndicator indicator, + Runnable callback) { + myContributor = contributor; + myPattern = pattern; + myAccumulator = accumulator; + myIndicator = indicator; + finishCallback = callback; + } + + + @Override + public void run() { + LOG.debug("Search task started for contributor ", myContributor); + try { + boolean repeat; + do { + ProgressIndicator wrapperIndicator = new SensitiveProgressWrapper(myIndicator); + try { + Runnable runnable = (myContributor instanceof WeightedSearchEverywhereContributor) + ? () -> ((WeightedSearchEverywhereContributor)myContributor).fetchWeightedElements(myPattern, wrapperIndicator, + descriptor -> processFoundItem(descriptor.getItem(), descriptor.getWeight(), wrapperIndicator)) + : () -> myContributor.fetchElements(myPattern, wrapperIndicator, + element -> { + int priority = myContributor.getElementPriority(Objects.requireNonNull(element), myPattern); + return processFoundItem(element, priority, wrapperIndicator); + }); + + ProgressManager.getInstance().runProcess(runnable, wrapperIndicator); + } + catch (ProcessCanceledException ignore) {} + repeat = !myIndicator.isCanceled() && wrapperIndicator.isCanceled(); + } + while (repeat); + + if (myIndicator.isCanceled()) { + return; + } + myAccumulator.contributorFinished(myContributor); + } + finally { + finishCallback.run(); + } + LOG.debug("Search task finished for contributor ", myContributor); + } + + private boolean processFoundItem(Item element, int priority, ProgressIndicator wrapperIndicator) { + try { + if (element == null) { + LOG.debug("Skip null element"); + return true; + } + + boolean added = myAccumulator.addElement(element, myContributor, priority, wrapperIndicator); + if (!added) { + myAccumulator.setContributorHasMore(myContributor, true); + } + return added; + } + catch (InterruptedException e) { + LOG.warn("Search task was interrupted"); + return false; + } + } + } + + private static class ResultsAccumulator { + + private final Map, Collection> mySections; + private final Listener myListener; + private final Executor myNotificationExecutor; + private final SEResultsEqualityProvider myEqualityProvider; + private final ProgressIndicator myProgressIndicator; + private final Map, Integer> sectionsLimits; + private final Map, Condition> conditionsMap; + private final Map, Boolean> hasMoreMap = new ConcurrentHashMap<>(); + private final Set> finishedContributorsSet = ContainerUtil.newConcurrentSet(); + private final Lock lock = new ReentrantLock(); + private volatile boolean mySearchFinished = false; + + ResultsAccumulator(Map, Integer> contributorsAndLimits, + SEResultsEqualityProvider equalityProvider, Listener listener, Executor notificationExecutor, + ProgressIndicator progressIndicator) { + this(contributorsAndLimits.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey(), entry -> new ArrayList<>(entry.getValue()))), + contributorsAndLimits, equalityProvider, listener, notificationExecutor, progressIndicator); + } + + ResultsAccumulator(Map, Collection> alreadyFound, + Map, Integer> contributorsAndLimits, + SEResultsEqualityProvider equalityProvider, + Listener listener, + Executor notificationExecutor, + ProgressIndicator progressIndicator) { + mySections = alreadyFound; + myEqualityProvider = equalityProvider; + myListener = listener; + myNotificationExecutor = notificationExecutor; + myProgressIndicator = progressIndicator; + + sectionsLimits = new HashMap<>(contributorsAndLimits); + conditionsMap = contributorsAndLimits.keySet().stream().collect(Collectors.toMap(Function.identity(), c -> lock.newCondition())); + } + + public void setContributorHasMore(SearchEverywhereContributor contributor, boolean hasMore) { + hasMoreMap.put(contributor, hasMore); + } + + public boolean addElement(Object element, SearchEverywhereContributor contributor, int priority, ProgressIndicator indicator) throws InterruptedException { + SearchEverywhereFoundElementInfo newElementInfo = new SearchEverywhereFoundElementInfo(element, priority, contributor); + Condition condition = conditionsMap.get(contributor); + Collection section = mySections.get(contributor); + int limit = sectionsLimits.get(contributor); + + lock.lock(); + try { + while (section.size() >= limit && !mySearchFinished) { + indicator.checkCanceled(); + condition.await(100, TimeUnit.MILLISECONDS); + } + + if (mySearchFinished) { + return false; + } + + Map> otherElementsMap = getActionsWithOtherElements(newElementInfo); + if (otherElementsMap.get(REPLACE).isEmpty() && !otherElementsMap.get(SKIP).isEmpty()) { + LOG.debug(String.format("Element %s for contributor %s was skipped", element.toString(), contributor.getSearchProviderId())); + return true; + } + + section.add(newElementInfo); + runInNotificationExecutor(() -> myListener.elementsAdded(Collections.singletonList(newElementInfo))); + + List toRemove = new ArrayList<>(otherElementsMap.get(REPLACE)); + toRemove.forEach(info -> { + Collection list = mySections.get(info.getContributor()); + Condition listCondition = conditionsMap.get(info.getContributor()); + list.remove(info); + LOG.debug(String.format("Element %s for contributor %s is removed", info.getElement().toString(), info.getContributor().getSearchProviderId())); + listCondition.signal(); + }); + runInNotificationExecutor(() -> myListener.elementsRemoved(toRemove)); + + if (section.size() >= limit) { + stopSearchIfNeeded(); + } + return true; + } + finally { + lock.unlock(); + } + } + + public void contributorFinished(SearchEverywhereContributor contributor) { + lock.lock(); + try { + finishedContributorsSet.add(contributor); + stopSearchIfNeeded(); + } + finally { + lock.unlock(); + } + } + + public void searchFinished() { + runInNotificationExecutor(() -> myListener.searchFinished(hasMoreMap)); + } + + public void stop() { + lock.lock(); + try { + mySearchFinished = true; + conditionsMap.values().forEach(Condition::signalAll); + } + finally { + lock.unlock(); + } + } + + /** + * could be used only when current thread owns {@link #lock} + */ + private void stopSearchIfNeeded() { + if (mySections.keySet().stream().allMatch(contributor -> isContributorFinished(contributor))) { + mySearchFinished = true; + conditionsMap.values().forEach(Condition::signalAll); + } + } + + private boolean isContributorFinished(SearchEverywhereContributor contributor) { + if (finishedContributorsSet.contains(contributor)) { + return true; + } + + Integer limit = sectionsLimits.get(contributor); + return limit == null || mySections.get(contributor).size() >= limit; + } + + private Map> getActionsWithOtherElements( + SearchEverywhereFoundElementInfo newElement) { + Map> res = new EnumMap<>( + SEResultsEqualityProvider.SEEqualElementsActionType.class); + res.put(REPLACE, new ArrayList<>()); + res.put(SKIP, new ArrayList<>()); + mySections.values() + .stream() + .flatMap(Collection::stream) + .forEach(info -> { + SEResultsEqualityProvider.SEEqualElementsActionType action = myEqualityProvider.compareItems(newElement, info); + if (action != SEResultsEqualityProvider.SEEqualElementsActionType.DO_NOTHING) { + res.get(action).add(info); + } + }); + + return res; + } + + private void runInNotificationExecutor(Runnable runnable) { + myNotificationExecutor.execute(() -> { + if (!myProgressIndicator.isCanceled()) { + runnable.run(); + } + }); + } + } + + private static class ProgressIndicatorWithCancelListener extends ProgressIndicatorBase { + + private volatile Runnable cancelCallback = () -> {}; + + private void setCancelCallback(Runnable cancelCallback) { + this.cancelCallback = cancelCallback; + } + + @Override + protected void onRunningChange() { + if (isCanceled()) { + cancelCallback.run(); + } + } + } +} 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 new file mode 100644 index 000000000000..cbb07eb4ff5b --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SEListSelectionTracker.java @@ -0,0 +1,108 @@ +// 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; + + 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 new file mode 100644 index 000000000000..6a0949b3e01f --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SESearcher.java @@ -0,0 +1,29 @@ +// 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 new file mode 100644 index 000000000000..68899326d0c7 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/SearchEverywhereUIMixedResults.java @@ -0,0 +1,1534 @@ +// 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.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.ide.util.gotoByName.SearchEverywhereConfiguration; +import com.intellij.internal.statistic.eventLog.FeatureUsageData; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.actionSystem.ex.AnActionListener; +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.Pair; +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.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.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> myShownContributors; + + private SearchListModel myListModel; + + private SETab mySelectedTab; + private final List myTabs = new ArrayList<>(); + private final Function myShortcutSupplier; + + private boolean myEverywhereAutoSet = true; + private String myNotFoundString; + + private JBPopup myHint; + + private final SESearcher mySearcher; + private final ThrottlingListenerWrapper myBufferedListener; + private ProgressIndicator mySearchProgressIndicator; + + private final SEListSelectionTracker mySelectionTracker; + private final PersistentSearchEverywhereContributorFilter myContributorsFilter; + private ActionToolbar myToolbar; + + public SearchEverywhereUIMixedResults(@NotNull Project project, + @NotNull List> contributors) { + this(project, contributors, s -> null); + } + + public SearchEverywhereUIMixedResults(@NotNull Project project, + @NotNull List> 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); + myShownContributors = contributors; + myShortcutSupplier = shortcutSupplier; + Map namesMap = ContainerUtil.map2Map(contributors, c -> Pair.create(c.getSearchProviderId(), c.getFullGroupName())); + myContributorsFilter = new PersistentSearchEverywhereContributorFilter<>( + ContainerUtil.map(contributors, c -> c.getSearchProviderId()), + SearchEverywhereConfiguration.getInstance(project), + namesMap::get, c -> null); + + 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 CompositeCellRenderer createCellRenderer() { + return new CompositeCellRenderer(); + } + + @NotNull + @Override + public JBList createList() { + myListModel = new SearchListModel(); + addListDataListener(myListModel); + + return new JBList<>(myListModel); + } + + public void toggleEverywhereFilter() { + myEverywhereAutoSet = false; + if (mySelectedTab.everywhereAction == null) return; + if (!mySelectedTab.everywhereAction.canToggleEverywhere()) return; + mySelectedTab.everywhereAction.setEverywhere( + !mySelectedTab.everywhereAction.isEverywhere()); + myToolbar.updateActionsImmediately(); + } + + private void setEverywhereAuto(boolean everywhere) { + myEverywhereAutoSet = true; + if (mySelectedTab.everywhereAction == null) return; + if (!mySelectedTab.everywhereAction.canToggleEverywhere()) return; + mySelectedTab.everywhereAction.setEverywhere(everywhere); + myToolbar.updateActionsImmediately(); + } + + private boolean isEverywhere() { + if (mySelectedTab.everywhereAction == null) return true; + return mySelectedTab.everywhereAction.isEverywhere(); + } + + private boolean canToggleEverywhere() { + if (mySelectedTab.everywhereAction == null) return false; + return mySelectedTab.everywhereAction.canToggleEverywhere(); + } + + public void switchToContributor(@NotNull String contributorID) { + SETab selectedTab = myTabs.stream() + .filter(tab -> tab.getID().equals(contributorID)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(String.format("Contributor %s is not supported", contributorID))); + switchToTab(selectedTab); + } + + private void switchToNextTab() { + int currentIndex = myTabs.indexOf(mySelectedTab); + SETab nextTab = currentIndex == myTabs.size() - 1 ? myTabs.get(0) : myTabs.get(currentIndex + 1); + switchToTab(nextTab); + } + + private void switchToPrevTab() { + int currentIndex = myTabs.indexOf(mySelectedTab); + SETab prevTab = currentIndex == 0 ? myTabs.get(myTabs.size() - 1) : myTabs.get(currentIndex - 1); + switchToTab(prevTab); + } + + private void switchToTab(SETab tab) { + boolean prevTabIsAll = mySelectedTab != null && isAllTabSelected(); + mySelectedTab = tab; + boolean nextTabIsAll = isAllTabSelected(); + + if (myEverywhereAutoSet && isEverywhere() && canToggleEverywhere()) { + setEverywhereAuto(false); + } + + updateSearchFieldAdvertisement(); + + if (prevTabIsAll != nextTabIsAll) { + //reset cell renderer to show/hide group titles in "All" tab + myResultsList.setCellRenderer(myResultsList.getCellRenderer()); + } + if (myToolbar != null) { + myToolbar.updateActionsImmediately(); + } + repaint(); + rebuildList(); + } + + 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; + + Boolean commandsSupported = mySelectedTab.getContributor() + .map(contributor -> !contributor.getSupportedCommands().isEmpty()) + .orElse(true); + + String advertisementText; + if (commandsSupported) { + advertisementText = IdeBundle.message("searcheverywhere.textfield.hint", SearchTopHitProvider.getTopHitAccelerator()); + } + else { + advertisementText = mySelectedTab.getContributor().map(c -> c.getAdvertisement()).orElse(null); + } + + mySearchField.remove(myAdvertisementLabel); + if (advertisementText != null) { + myAdvertisementLabel.setText(advertisementText); + mySearchField.add(myAdvertisementLabel, BorderLayout.EAST); + } + } + + public String getSelectedContributorID() { + return mySelectedTab.getID(); + } + + @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); + } + } + + private boolean isAllTabSelected() { + return SearchEverywhereManagerImpl.ALL_CONTRIBUTORS_GROUP_ID.equals(getSelectedContributorID()); + } + + @Override + @NotNull + protected JPanel createSettingsPanel() { + DefaultActionGroup actionGroup = new DefaultActionGroup(); + actionGroup.addAction(new ActionGroup() { + @Override + public AnAction @NotNull [] getChildren(@Nullable AnActionEvent e) { + if (e == null || mySelectedTab == null) return EMPTY_ARRAY; + return mySelectedTab.actions.toArray(EMPTY_ARRAY); + } + }); + actionGroup.addAction(new ShowInFindToolWindowAction()); + + myToolbar = ActionManager.getInstance().createActionToolbar("search.everywhere.toolbar", actionGroup, true); + myToolbar.setLayoutPolicy(ActionToolbar.NOWRAP_LAYOUT_POLICY); + myToolbar.updateActionsImmediately(); + JComponent toolbarComponent = myToolbar.getComponent(); + toolbarComponent.setOpaque(false); + toolbarComponent.setBorder(JBUI.Borders.empty(2, 18, 2, 9)); + return (JPanel)toolbarComponent; + } + + @NotNull + @Override + protected String getInitialHint() { + return 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() { + JPanel contributorsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + contributorsPanel.setOpaque(false); + + SETab allTab = new SETab(null); + contributorsPanel.add(allTab); + myTabs.add(allTab); + + myShownContributors.stream() + .filter(SearchEverywhereContributor::isShownInSeparateTab) + .forEach(contributor -> { + SETab tab = new SETab(contributor); + contributorsPanel.add(tab); + myTabs.add(tab); + }); + + return contributorsPanel; + } + + private class SETab extends JLabel { + final SearchEverywhereContributor contributor; + final List actions; + final SearchEverywhereToggleAction everywhereAction; + + SETab(@Nullable SearchEverywhereContributor contributor) { + super(contributor == null ? IdeBundle.message("searcheverywhere.allelements.tab.name") : contributor.getGroupName()); + this.contributor = contributor; + updateTooltip(); + Runnable onChanged = () -> { + myToolbar.updateActionsImmediately(); + rebuildList(); + }; + if (contributor == null) { + String actionText = IdeUICustomization.getInstance().projectMessage("checkbox.include.non.project.items"); + actions = Arrays.asList(new CheckBoxSearchEverywhereToggleAction(actionText) { + final SearchEverywhereManagerImpl seManager = (SearchEverywhereManagerImpl)SearchEverywhereManager.getInstance(myProject); + @Override + public boolean isEverywhere() { + return seManager.isEverywhere(); + } + + @Override + public void setEverywhere(boolean state) { + seManager.setEverywhere(state); + myTabs.stream() + .filter(tab -> tab != SETab.this) + .forEach(tab -> tab.everywhereAction.setEverywhere(state)); + onChanged.run(); + } + }, new FiltersAction(myContributorsFilter, onChanged)); + } + else { + actions = new ArrayList<>(contributor.getActions(onChanged)); + } + everywhereAction = (SearchEverywhereToggleAction)ContainerUtil.find(actions, o -> o instanceof SearchEverywhereToggleAction); + Insets insets = JBUI.CurrentTheme.BigPopup.tabInsets(); + setBorder(JBUI.Borders.empty(insets.top, insets.left, insets.bottom, insets.right)); + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + switchToTab(SETab.this); + String reportableID = getContributor() + .map(SearchEverywhereUsageTriggerCollector::getReportableContributorID) + .orElse(SearchEverywhereManagerImpl.ALL_CONTRIBUTORS_GROUP_ID); + FeatureUsageData data = SearchEverywhereUsageTriggerCollector + .createData(reportableID) + .addInputEvent(e); + featureTriggered(SearchEverywhereUsageTriggerCollector.TAB_SWITCHED, data); + } + }); + } + + private void updateTooltip() { + String shortcut = myShortcutSupplier.apply(getID()); + if (shortcut != null) { + setToolTipText(shortcut); + } + } + + public String getID() { + return getContributor() + .map(SearchEverywhereContributor::getSearchProviderId) + .orElse(SearchEverywhereManagerImpl.ALL_CONTRIBUTORS_GROUP_ID); + } + + public Optional> getContributor() { + return Optional.ofNullable(contributor); + } + + @Override + public Dimension getPreferredSize() { + Dimension size = super.getPreferredSize(); + size.height = JBUIScale.scale(29); + return size; + } + + @Override + public boolean isOpaque() { + return mySelectedTab == this; + } + + @Override + public Color getBackground() { + return mySelectedTab == this + ? JBUI.CurrentTheme.BigPopup.selectedTabColor() + : super.getBackground(); + } + + @Override + public Color getForeground() { + return mySelectedTab == this + ? JBUI.CurrentTheme.BigPopup.selectedTabTextColor() + : super.getForeground(); + } + } + + 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 = mySelectedTab.getContributor() + .map(contributor -> contributor.filterControlSymbols(rawPattern)) + .orElse(rawPattern); + + MinusculeMatcher matcher = + NameUtil.buildMatcherWithFallback("*" + rawPattern, "*" + namePattern, NameUtil.MatchingCaseSensitivity.NONE); + MatcherHolder.associateMatcher(myResultsList, matcher); + + Map, Integer> contributorsMap = new HashMap<>(); + Optional> selectedContributor = mySelectedTab.getContributor(); + if (selectedContributor.isPresent()) { + contributorsMap.put(selectedContributor.get(), SINGLE_CONTRIBUTOR_ELEMENTS_LIMIT); + } + else { + contributorsMap.putAll(getAllTabContributors().stream().collect(Collectors.toMap(c -> c, c -> MULTIPLE_CONTRIBUTORS_ELEMENTS_LIMIT))); + } + + List> contributors = DumbService.getInstance(myProject).filterByDumbAwareness(contributorsMap.keySet()); + if (contributors.isEmpty() && DumbService.isDumb(myProject)) { + myResultsList.setEmptyText(IdeBundle.message("searcheverywhere.indexing.mode.not.supported", + mySelectedTab.getText(), + ApplicationNamesInfo.getInstance().getFullProductName())); + myListModel.clear(); + return; + } + if (contributors.size() != contributorsMap.size()) { + myResultsList.setEmptyText(IdeBundle.message("searcheverywhere.indexing.incomplete.results", + mySelectedTab.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 -> { + switchToNextTab(); + triggerTabSwitched(e); + }; + Consumer prevTabAction = e -> { + 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()) { + switchToPrevTab(); + } + else { + 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_NEXT_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 (myEverywhereAutoSet && isEverywhere() && canToggleEverywhere() && !newPatternContainsPrevious) { + myNotFoundString = null; + setEverywhereAuto(false); + return; + } + } + + rebuildList(); + } + }); + + myResultsList.addListSelectionListener(e -> { + Object selectedValue = myResultsList.getSelectedValue(); + if (selectedValue != null && myHint != null && myHint.isVisible()) { + updateHint(selectedValue); + } + + showDescriptionForIndex(myResultsList.getSelectedIndex()); + }); + + MessageBusConnection projectBusConnection = myProject.getMessageBus().connect(this); + projectBusConnection.subscribe(DumbService.DUMB_MODE, new DumbService.DumbModeListener() { + @Override + public void exitDumbMode() { + ApplicationManager.getApplication().invokeLater(() -> { + updateSearchFieldAdvertisement(); + rebuildList(); + }); + } + }); + projectBusConnection.subscribe(AnActionListener.TOPIC, new AnActionListener() { + @Override + public void afterActionPerformed(@NotNull AnAction action, @NotNull DataContext dataContext, @NotNull AnActionEvent event) { + if (action == mySelectedTab.everywhereAction && event.getInputEvent() != null) { + myEverywhereAutoSet = false; + } + } + }); + + ApplicationManager.getApplication() + .getMessageBus() + .connect(this) + .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 = mySelectedTab.getContributor() + .map(SearchEverywhereUsageTriggerCollector::getReportableContributorID) + .orElse(SearchEverywhereManagerImpl.ALL_CONTRIBUTORS_GROUP_ID); + + 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 selectedTabContributorID = mySelectedTab.getContributor() + .map(SearchEverywhereUsageTriggerCollector::getReportableContributorID) + .orElse(SearchEverywhereManagerImpl.ALL_CONTRIBUTORS_GROUP_ID); + + String reportableContributorID = SearchEverywhereUsageTriggerCollector.getReportableContributorID(contributor); + FeatureUsageData data = SearchEverywhereUsageTriggerCollector.createData(reportableContributorID, selectedTabContributorID, 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 = isAllTabSelected() ? MULTIPLE_CONTRIBUTORS_ELEMENTS_LIMIT : SINGLE_CONTRIBUTOR_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)); + + 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(); + } + + @NotNull + private List> getAllTabContributors() { + return ContainerUtil.filter(myShownContributors, contributor -> myContributorsFilter.isSelected(contributor.getSearchProviderId())); + } + + @NotNull + private Collection> getContributorsForCurrentTab() { + return isAllTabSelected() ? getAllTabContributors() : Collections.singleton(mySelectedTab.getContributor().get()); + } + + @TestOnly + public Future> findElementsForPattern(String pattern) { + CompletableFuture> future = new CompletableFuture<>(); + mySearchListener.setTestCallback(list -> { + future.complete(list); + mySearchListener.setTestCallback(null); + }); + mySearchField.setText(pattern); + return future; + } + + 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; + + 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 hasMoreContributors.get(contributor); + } + + public void addElements(List items) { + if (items.isEmpty()) { + return; + } + + items = items.stream() + .sorted(SearchEverywhereFoundElementInfo.COMPARATOR.reversed()) + .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) { + listElements.sort(SearchEverywhereFoundElementInfo.COMPARATOR.reversed()); + 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 = getContributorsForCurrentTab(); + 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)); + + closePopup(); + 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()) { + int usageCount = foundElements.size() + alreadyFoundCount; + UsageViewManagerImpl.showTooManyUsagesWarningLater( + getProject(), tooManyUsagesStatus, progressIndicator, presentation, usageCount, 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) { + progressIndicator.cancel(); + } + }); + } + else { + showInFindWindow(targets, usages, presentation); + } + } + + 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) { + SearchEverywhereContributor contributor = mySelectedTab == null ? null : mySelectedTab.contributor; + e.getPresentation().setEnabled(contributor == null || contributor.showInFindResults()); + 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(getContributorsForCurrentTab(), typedCommand); + return completions.isEmpty() ? null : completions.get(0); + }); + + return Optional.ofNullable(command); + } + + return Optional.empty(); + } + } + + private String getNotFoundText() { + return mySelectedTab.getContributor() + .map(c -> IdeBundle.message("searcheverywhere.nothing.found.for.contributor.anywhere", + c.getFullGroupName().toLowerCase(Locale.ROOT))) + .orElse(IdeBundle.message("searcheverywhere.nothing.found.for.all.anywhere")); + } + + 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(getSelectedContributorID()); + 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) { + if (myResultsList.isEmpty() || myListModel.isResultsExpired()) { + if (myEverywhereAutoSet && !isEverywhere() && canToggleEverywhere() && !getSearchPattern().isEmpty()) { + setEverywhereAuto(true); + myNotFoundString = getSearchPattern(); + return; + } + + hideHint(); + if (myListModel.isResultsExpired()) { + myListModel.clear(); + } + } + + myResultsList.setEmptyText(getSearchPattern().isEmpty() ? "" : getNotFoundText()); + hasMoreContributors.forEach(myListModel::setHasMore); + + 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 new file mode 100644 index 000000000000..f20feacdf360 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/actions/searcheverywhere/mixed/ThrottlingListenerWrapper.java @@ -0,0 +1,142 @@ +// 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 new file mode 100644 index 000000000000..e87427f049a3 --- /dev/null +++ b/platform/lang-impl/testSources/com/intellij/ide/actions/searcheverywhere/MixingMultiThreadSearchTest.groovy @@ -0,0 +1,313 @@ +// 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.application.Experiments +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.util.Alarm +import com.intellij.util.Processor +import org.jetbrains.annotations.NotNull +import org.junit.Assert + +import javax.swing.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Phaser +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import java.util.concurrent.atomic.AtomicBoolean + +class MixingMultiThreadSearchTest extends BasePlatformTestCase { + + private static final String MORE_ITEM = "...MORE" + private static final Collection ourEqualityProviders = Collections.singleton(new TrivialElementsEqualityProvider()) + + @Override + protected void setUp() throws Exception { + super.setUp() + Experiments.getInstance().setFeatureEnabled("search.everywhere.mixed.results", true) + } + + @Override + protected void tearDown() throws Exception { + super.tearDown() + Experiments.getInstance().setFeatureEnabled("search.everywhere.mixed.results", false) + } + + void "test simple without collisions"() { + def contributors = [ + (createTestContributor("test1", 0, "item1_1", "item1_2", "item1_3", "item1_4", "item1_5", "item1_6", "item1_7", "item1_8", "item1_9", "item1_10", "item1_11", "item1_12", "item1_13", "item1_14", "item1_15")) : 12, + (createTestContributor("test2", 0, "item2_1", "item2_2", "item2_3", "item2_4", "item2_5", "item2_6", "item2_7", "item2_8", "item2_9", "item2_10", "item2_11", "item2_12")) : 10, + (createTestContributor("test3", 0, "item3_1", "item3_2", "item3_3", "item3_4", "item3_5", "item3_6", "item3_7", "item3_8")) : 10, + (createTestContributor("test4", 0, "item4_1", "item4_2", "item4_3", "item4_4", "item4_5", "item4_6", "item4_7", "item4_8", "item4_9", "item4_10", "item4_11", "item4_12", "item4_13")) : 11, + (createTestContributor("test5", 0)) : 10, + (createTestContributor("test6", 0, "item6_1", "item6_2", "item6_3", "item6_4", "item6_5", "item6_6", "item6_7", "item6_8", "item6_9", "item6_10", "item6_11", "item6_12", "item6_13")) : 10, + (createTestContributor("test7", 0, "item7_1", "item7_2", "item7_3", "item7_4", "item7_5", "item7_6", "item7_7", "item7_8", "item7_9", "item7_10")) : 10, + (createTestContributor("test8", 0, "item8_1", "item8_2", "item8_3", "item8_4", "item8_5")) : 10, + (createTestContributor("test9", 0, "item9_1", "item9_2", "item9_3", "item9_4", "item9_5")) : 3, + (createTestContributor("test10", 0, "item10_1", "item10_2", "item10_3", "item10_4", "item10_5")) : 5 + ] + def results = [ + "test1" : ["item1_1", "item1_2", "item1_3", "item1_4", "item1_5", "item1_6", "item1_7", "item1_8", "item1_9", "item1_10", "item1_11", "item1_12", MORE_ITEM], + "test2" : ["item2_1", "item2_2", "item2_3", "item2_4", "item2_5", "item2_6", "item2_7", "item2_8", "item2_9", "item2_10", MORE_ITEM], + "test3" : ["item3_1", "item3_2", "item3_3", "item3_4", "item3_5", "item3_6", "item3_7", "item3_8"], + "test4" : ["item4_1", "item4_2", "item4_3", "item4_4", "item4_5", "item4_6", "item4_7", "item4_8", "item4_9", "item4_10", "item4_11", MORE_ITEM], + "test5" : [], + "test6" : ["item6_1", "item6_2", "item6_3", "item6_4", "item6_5", "item6_6", "item6_7", "item6_8", "item6_9", "item6_10", MORE_ITEM], + "test7" : ["item7_1", "item7_2", "item7_3", "item7_4", "item7_5", "item7_6", "item7_7", "item7_8", "item7_9", "item7_10"], + "test8" : ["item8_1", "item8_2", "item8_3", "item8_4", "item8_5"], + "test9" : ["item9_1", "item9_2", "item9_3", MORE_ITEM], + "test10" : ["item10_1", "item10_2", "item10_3", "item10_4", "item10_5"] + ] + + testScenario(new Scenario(contributors, results)) + } + + void "test simple without MORE items"() { + def contributors = [ + (createTestContributor("test1", 0, "item1_1", "item1_2", "item1_3", "item1_4", "item1_5", "item1_6", "item1_7", "item1_8", "item1_9", "item1_10")) : 10, + (createTestContributor("test2", 0, "item2_1", "item2_2", "item2_3", "item2_4", "item2_5", "item2_6", "item2_7", "item2_8", "item2_9", "item2_10", "item2_11", "item2_12")) : 20, + (createTestContributor("test3", 0, "item3_1", "item3_2", "item3_3", "item3_4", "item3_5", "item3_6", "item3_7", "item3_8")) : 10 + ] + def results = [ + "test1" : ["item1_1", "item1_2", "item1_3", "item1_4", "item1_5", "item1_6", "item1_7", "item1_8", "item1_9", "item1_10"], + "test2" : ["item2_1", "item2_2", "item2_3", "item2_4", "item2_5", "item2_6", "item2_7", "item2_8", "item2_9", "item2_10", "item2_11", "item2_12"], + "test3" : ["item3_1", "item3_2", "item3_3", "item3_4", "item3_5", "item3_6", "item3_7", "item3_8"] + ] + + testScenario(new Scenario(contributors, results)) + } + + void "test empty results"() { + def contributors = [ + (createTestContributor("test1", 0)) : 10, + (createTestContributor("test2", 0)) : 10, + (createTestContributor("test3", 0)) : 10, + (createTestContributor("test4", 0)) : 10, + (createTestContributor("test5", 0)) : 10 + ] + def results = [ + "test1" : [], + "test2" : [], + "test3" : [], + "test4" : [], + "test5" : [] + ] + + testScenario(new Scenario(contributors, results)) + } + + void "test one contributor"() { + testScenario(new Scenario( + [(createTestContributor("test1", 0, "item1_1", "item1_2", "item1_3", "item1_4", "item1_5", "item1_6", "item1_7", "item1_8", "item1_9", "item1_10", "item1_11", "item1_12", "item1_13", "item1_14", "item1_15")) : 10], + ["test1": ["item1_1", "item1_2", "item1_3", "item1_4", "item1_5", "item1_6", "item1_7", "item1_8", "item1_9", "item1_10", MORE_ITEM]]) + ) + } + + void "test one contributor with no MORE item"() { + testScenario(new Scenario( + [(createTestContributor("test1", 0, "item1_1", "item1_2", "item1_3", "item1_4", "item1_5", "item1_6", "item1_7", "item1_8", "item1_9", "item1_10")) : 10], + ["test1": ["item1_1", "item1_2", "item1_3", "item1_4", "item1_5", "item1_6", "item1_7", "item1_8", "item1_9", "item1_10"]]) + ) + } + + void "test one contributor with empty results"() { + testScenario(new Scenario( + [(createTestContributor("test1", 0)) : 10], + ["test1":[]]) + ) + } + + void "test collisions scenario"() { + def contributors = [ + (createTestContributor("test1", 0, "item1_1", "item1_2", "item1_3", "item1_4", "item1_5", "item1_6", "item1_7", "item1_8", "item1_9", "item1_10", "item1_11", "item1_12", "item1_13", "item1_14", "item1_15")) : 12, + (createTestContributor("test2", 10, "item2_1", "item2_2", "duplicateItem1", "duplicateItem2", "item2_3", "item2_4", "item2_5", "item2_6", "item2_7", "item2_8", "item2_9", "item2_10", "item2_11", "item2_12")) : 10, + (createTestContributor("test3", 8, "item3_1", "item3_2", "item3_3", "item3_4", "duplicateItem1", "item3_5", "item3_6", "item3_7", "item3_8", "duplicateItem2", "duplicateItem3")) : 10, + (createTestContributor("test4", 15, "item4_1", "item4_2", "duplicateItem2", "duplicateItem3", "item4_3", "item4_4", "item4_5", "item4_6", "item4_7", "item4_8", "item4_9")) : 10, + (createTestContributor("test6", 20, "item6_1", "item6_2", "item6_3", "item6_4", "duplicateItem3", "item6_5", "item6_6", "item6_7", "item6_8", "item6_9", "item6_10", "item6_11", "duplicateItem4", "item6_12", "item6_13")) : 10, + (createTestContributor("test7", 5, "item7_1", "item7_2", "duplicateItem3", "item7_3", "item7_4", "item7_5", "item7_6", "item7_7", "item7_8", "item7_9", "item7_10")) : 10, + (createTestContributor("test8", 10, "item8_1", "item8_2", "item8_3", "item8_4", "item8_5", "duplicateItem4")) : 10, + (createTestContributor("test9", 15, "item9_1", "item9_2", "item9_3", "item9_4", "duplicateItem5", "duplicateItem6")) : 5, + (createTestContributor("test10", 10, "item10_1", "item10_2", "item10_3", "item10_4", "duplicateItem5", "duplicateItem6")) : 5 + ] + def results = [ + "test1": ["item1_1", "item1_2", "item1_3", "item1_4", "item1_5", "item1_6", "item1_7", "item1_8", "item1_9", "item1_10", "item1_11", "item1_12", MORE_ITEM], + "test2": ["item2_1", "item2_2", "duplicateItem1", "item2_3", "item2_4", "item2_5", "item2_6", "item2_7", "item2_8", "item2_9", MORE_ITEM], + "test3": ["item3_1", "item3_2", "item3_3", "item3_4", "item3_5", "item3_6", "item3_7", "item3_8"], + "test4": ["item4_1", "item4_2", "duplicateItem2", "item4_3", "item4_4", "item4_5", "item4_6", "item4_7", "item4_8", "item4_9"], + "test6": ["item6_1", "item6_2", "item6_3", "item6_4", "duplicateItem3", "item6_5", "item6_6", "item6_7", "item6_8", "item6_9", MORE_ITEM], + "test7": ["item7_1", "item7_2", "item7_3", "item7_4", "item7_5", "item7_6", "item7_7", "item7_8", "item7_9", "item7_10"], + "test8": ["item8_1", "item8_2", "item8_3", "item8_4", "item8_5", "duplicateItem4"], + "test9": ["item9_1", "item9_2", "item9_3", "item9_4", "duplicateItem5", MORE_ITEM], + "test10": ["item10_1", "item10_2", "item10_3", "item10_4", "duplicateItem6"] + ] + testScenario(new Scenario(contributors, results)) + } + + void "test only collisions"() { + def contributors = [ + (createTestContributor("test1", 10, "item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item9", "item10", "item11", "item12")) : 5, + (createTestContributor("test2", 9, "item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item9", "item10", "item11", "item12")) : 5, + (createTestContributor("test3", 8, "item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item9", "item10", "item11", "item12")) : 5, + (createTestContributor("test4", 7, "item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item9", "item10", "item11", "item12")) : 5, + (createTestContributor("test5", 6, "item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item9", "item10", "item11", "item12")) : 5 + ] + def results = [ + "test1" : ["item1", "item2", "item3", "item4", "item5", MORE_ITEM], + "test2" : ["item6", "item7", "item8", "item9", "item10", MORE_ITEM], + "test3" : ["item11", "item12"], + "test4" : [], + "test5" : [] + ] + testScenario(new Scenario(contributors, results)) + } + + 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) + ProgressIndicator indicator = searcher.search(scenario.contributorsAndLimits, "tst") + + try { + collector.awaitFinish(1000) + } + catch (TimeoutException ignored) { + Assert.fail("Search timeout exceeded") + } + catch (InterruptedException ignored) { + } + finally { + indicator.cancel() + } + scenario.results.forEach({ contributorId, results -> + List values = collector.getContributorValues(contributorId) + Assert.assertEquals(results, values) + }) + collector.clear() + } + + private static SearchEverywhereContributor createTestContributor(String id, int fixedPriority, String... items) { + return new SearchEverywhereContributor() { + @NotNull + @Override + String getSearchProviderId() { + return id + } + + @NotNull + @Override + String getGroupName() { + return id + } + + @Override + int getSortWeight() { + return 0 + } + + @Override + boolean showInFindResults() { + return false + } + + @Override + int getElementPriority(@NotNull Object element, @NotNull String searchPattern) { + return fixedPriority + } + + @Override + void fetchElements(@NotNull String pattern, + @NotNull ProgressIndicator progressIndicator, + @NotNull Processor consumer) { + boolean flag = true + Iterator iterator = Arrays.asList(items).iterator() + while (flag && iterator.hasNext()) { + String next = iterator.next() + flag = consumer.process(next) + } + } + + @Override + boolean processSelectedItem(@NotNull Object selected, int modifiers, @NotNull String searchText) { + return false + } + + @NotNull + @Override + ListCellRenderer getElementsRenderer() { + throw new UnsupportedOperationException() + } + + @Override + Object getDataForItem(@NotNull Object element, @NotNull String dataId) { + return null + } + } + } + + static class Scenario { + private final Map, Integer> contributorsAndLimits + private final Map> results + + Scenario(Map, Integer> contributorsAndLimits, Map> results) { + this.contributorsAndLimits = contributorsAndLimits + this.results = results + } + } + + static class SearchResultsCollector implements SESearcher.Listener { + + private final Map> myMap = new ConcurrentHashMap<>() + private final AtomicBoolean myFinished = new AtomicBoolean(false) + private final Phaser myPhaser = new Phaser(2) + + List getContributorValues(String contributorId) { + List values = myMap.get(contributorId) + return values != null ? values : [] + } + + void awaitFinish(long timeout) throws TimeoutException, InterruptedException { + int phase = myPhaser.arrive() + myPhaser.awaitAdvanceInterruptibly(phase, timeout, TimeUnit.MILLISECONDS) + } + + void clear() { + myMap.clear() + myFinished.set(false) + } + + @Override + void elementsAdded(@NotNull List added) { + added.forEach({ info -> + List list = myMap.computeIfAbsent(info.getContributor().getSearchProviderId(), { s -> new ArrayList<>() }) + list.add((String)info.getElement()) + }) + } + + @Override + void elementsRemoved(@NotNull List removed) { + removed.forEach({ info -> + List list = myMap.get(info.getContributor().getSearchProviderId()) + Assert.assertNotNull("Trying to remove object, that wasn't added", list) + list.remove(info.getElement()) + }) + } + + @Override + void searchFinished(@NotNull Map, Boolean> hasMoreContributors) { + hasMoreContributors.entrySet() + .stream() + .filter({ entry -> entry.getValue() }) + .map({ entry -> entry.getKey() }) + .forEach({ contributor -> + List list = myMap.get(contributor.getSearchProviderId()) + Assert.assertNotNull("If section has MORE item it cannot be empty", list) + list.add(MORE_ITEM) + }) + + boolean set = myFinished.compareAndSet(false, true) + Assert.assertTrue("More than one finish event", set) + + myPhaser.arrive() + } + } +} diff --git a/platform/platform-resources/src/META-INF/PlatformExtensions.xml b/platform/platform-resources/src/META-INF/PlatformExtensions.xml index 3fa4cd9c4e93..8d43844d2d34 100644 --- a/platform/platform-resources/src/META-INF/PlatformExtensions.xml +++ b/platform/platform-resources/src/META-INF/PlatformExtensions.xml @@ -855,6 +855,10 @@ Defines whether to show "What's new" notification after IDE update or not. + + Use mixed list of results on All tab in Search Everywhere (instead of Files/Classes/Actions groups) + + diff --git a/platform/platform-resources/src/keymaps/ReSharper OSX.xml b/platform/platform-resources/src/keymaps/ReSharper OSX.xml index 60d4b7817a40..c202daf0e032 100755 --- a/platform/platform-resources/src/keymaps/ReSharper OSX.xml +++ b/platform/platform-resources/src/keymaps/ReSharper OSX.xml @@ -165,6 +165,10 @@ + + + + @@ -181,5 +185,9 @@ + + + + \ No newline at end of file