IDEA-230679 Search Everywhere: Show all results in one group on All tab

GitOrigin-RevId: 0ea547d1c7fca47046ecca20bd2c9bd94700e713
This commit is contained in:
Mikhail Sokolov
2020-05-22 12:39:09 +03:00
committed by intellij-monorepo-bot
parent 1c7303fd74
commit 4f7c69d4df
20 changed files with 2781 additions and 77 deletions
@@ -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
}
}
@@ -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;
@@ -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<SearchEverywhereContributor<Object>> 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<SearchEverywhereContributor<Object>> 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<Object> {
private final String myId
private final int mySortWeight
private final Map<Object, Integer> myElements = new HashMap<>()
StubContributor(String id, int sortWeight) {
myId = id
mySortWeight = sortWeight
}
void addElements(Map<Object, Integer> elements) {
myElements.putAll(elements)
}
@Override
void fetchWeightedElements(@NotNull String pattern,
@NotNull ProgressIndicator progressIndicator,
@NotNull Processor<? super FoundItemDescriptor<Object>> 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<? super Object> getElementsRenderer() {
return new DefaultListCellRenderer()
}
@Override
Object getDataForItem(@NotNull Object element, @NotNull String dataId) {
return null
}
}
}
@@ -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()) {
@@ -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;
}
@@ -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;
@@ -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<SearchEverywhereFoundElementInfo> 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());
};
}
@@ -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<? extends SearchEverywhereContributor<?>> 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);
}
}
@@ -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 <T> ElementsChooser<T> createChooser(@NotNull PersistentSearchEverywhereContributorFilter<T> filter,
@NotNull Runnable rebuildRunnable) {
ElementsChooser<T> res = new ElementsChooser<T>(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<T> 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) {
@@ -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 <T> ElementsChooser<T> createChooser(@NotNull PersistentSearchEverywhereContributorFilter<T> filter,
@NotNull Runnable rebuildRunnable) {
ElementsChooser<T> res = new ElementsChooser<T>(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<T> listener = (element, isMarked) -> {
filter.setSelected(element, isMarked);
rebuildRunnable.run();
};
res.addElementsMarkListener(listener);
return res;
}
}
}
@@ -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();
@@ -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;
}
@@ -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<? extends SEResultsEqualityProvider> equalityProviders) {
myListener = listener;
myNotificationExecutor = notificationExecutor;
myEqualityProvider = SEResultsEqualityProvider.composite(equalityProviders);
}
@Override
public ProgressIndicator search(@NotNull Map<? extends SearchEverywhereContributor<?>, Integer> contributorsAndLimits,
@NotNull String pattern) {
LOG.debug("Search started for pattern [", pattern, "]");
Collection<? extends SearchEverywhereContributor<?>> 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<ProgressIndicator, ResultsAccumulator> accumulatorSupplier = indicator ->
new ResultsAccumulator(contributorsAndLimits, myEqualityProvider, myListener, myNotificationExecutor, indicator);
return performSearch(contributors, pattern, accumulatorSupplier);
}
@Override
public ProgressIndicator findMoreItems(@NotNull Map<? extends SearchEverywhereContributor<?>, Collection<SearchEverywhereFoundElementInfo>> alreadyFound,
@NotNull Map<? extends SearchEverywhereContributor<?>, Integer> contributorsAndLimits,
@NotNull String pattern) {
Function<ProgressIndicator, ResultsAccumulator> accumulatorSupplier = indicator ->
new ResultsAccumulator(alreadyFound, contributorsAndLimits, myEqualityProvider, myListener, myNotificationExecutor, indicator);
return performSearch(contributorsAndLimits.keySet(), pattern, accumulatorSupplier);
}
@NotNull
private static ProgressIndicator performSearch(@NotNull Collection<? extends SearchEverywhereContributor<?>> contributors,
@NotNull String pattern,
@NotNull Function<ProgressIndicator, ResultsAccumulator> 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<Object>)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<Item> implements Runnable {
private final ResultsAccumulator myAccumulator;
private final Runnable finishCallback;
private final SearchEverywhereContributor<Item> myContributor;
private final String myPattern;
private final ProgressIndicator myIndicator;
private ContributorSearchTask(SearchEverywhereContributor<Item> 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<Item>)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<? extends SearchEverywhereContributor<?>, Collection<SearchEverywhereFoundElementInfo>> mySections;
private final Listener myListener;
private final Executor myNotificationExecutor;
private final SEResultsEqualityProvider myEqualityProvider;
private final ProgressIndicator myProgressIndicator;
private final Map<? extends SearchEverywhereContributor<?>, Integer> sectionsLimits;
private final Map<? extends SearchEverywhereContributor<?>, Condition> conditionsMap;
private final Map<SearchEverywhereContributor<?>, Boolean> hasMoreMap = new ConcurrentHashMap<>();
private final Set<SearchEverywhereContributor<?>> finishedContributorsSet = ContainerUtil.newConcurrentSet();
private final Lock lock = new ReentrantLock();
private volatile boolean mySearchFinished = false;
ResultsAccumulator(Map<? extends SearchEverywhereContributor<?>, 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<? extends SearchEverywhereContributor<?>, Collection<SearchEverywhereFoundElementInfo>> alreadyFound,
Map<? extends SearchEverywhereContributor<?>, 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<SearchEverywhereFoundElementInfo> 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<SEResultsEqualityProvider.SEEqualElementsActionType, Collection<SearchEverywhereFoundElementInfo>> 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<SearchEverywhereFoundElementInfo> toRemove = new ArrayList<>(otherElementsMap.get(REPLACE));
toRemove.forEach(info -> {
Collection<SearchEverywhereFoundElementInfo> 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<SEResultsEqualityProvider.SEEqualElementsActionType, Collection<SearchEverywhereFoundElementInfo>> getActionsWithOtherElements(
SearchEverywhereFoundElementInfo newElement) {
Map<SEResultsEqualityProvider.SEEqualElementsActionType, Collection<SearchEverywhereFoundElementInfo>> 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();
}
}
}
}
@@ -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<Object> 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<Object> 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();
}
}
@@ -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<? extends SearchEverywhereContributor<?>, Integer> contributorsAndLimits,
@NotNull String pattern);
ProgressIndicator findMoreItems(@NotNull Map<? extends SearchEverywhereContributor<?>, Collection<SearchEverywhereFoundElementInfo>> alreadyFound,
@NotNull Map<? extends SearchEverywhereContributor<?>, Integer> contributorsAndLimits,
@NotNull String pattern);
/**
* Search process listener interface
*/
interface Listener {
void elementsAdded(@NotNull List<? extends SearchEverywhereFoundElementInfo> list);
void elementsRemoved(@NotNull List<? extends SearchEverywhereFoundElementInfo> list);
void searchFinished(@NotNull Map<SearchEverywhereContributor<?>, Boolean> hasMoreContributors);
}
}
@@ -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.
* <br>
* 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<SearchEverywhereFoundElementInfo>, List<SearchEverywhereFoundElementInfo>> 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<? extends SearchEverywhereFoundElementInfo> list) {
ApplicationManager.getApplication().assertIsDispatchThread();
myBuffer.addEvent(new Event(Event.ADD, list));
scheduleFlushBuffer();
}
@Override
public void elementsRemoved(@NotNull List<? extends SearchEverywhereFoundElementInfo> list) {
ApplicationManager.getApplication().assertIsDispatchThread();
myBuffer.addEvent(new Event(Event.REMOVE, list));
scheduleFlushBuffer();
}
@Override
public void searchFinished(@NotNull Map<SearchEverywhereContributor<?>, 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<? extends SearchEverywhereFoundElementInfo> items;
Event(int type, List<? extends SearchEverywhereFoundElementInfo> items) {
this.type = type;
this.items = items;
}
}
private static class Buffer {
private final Queue<Event> myQueue = new ArrayDeque<>();
public void addEvent(Event event) {
myQueue.add(event);
}
public void flush(BiConsumer<? super List<SearchEverywhereFoundElementInfo>, ? super List<SearchEverywhereFoundElementInfo>> consumer) {
List<SearchEverywhereFoundElementInfo> added = new ArrayList<>();
List<SearchEverywhereFoundElementInfo> 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();
}
}
}
@@ -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<SEResultsEqualityProvider> 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<String> values = collector.getContributorValues(contributorId)
Assert.assertEquals(results, values)
})
collector.clear()
}
private static SearchEverywhereContributor<Object> createTestContributor(String id, int fixedPriority, String... items) {
return new SearchEverywhereContributor<Object>() {
@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<? super Object> consumer) {
boolean flag = true
Iterator<String> 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<? super Object> getElementsRenderer() {
throw new UnsupportedOperationException()
}
@Override
Object getDataForItem(@NotNull Object element, @NotNull String dataId) {
return null
}
}
}
static class Scenario {
private final Map<SearchEverywhereContributor<Object>, Integer> contributorsAndLimits
private final Map<String, List<String>> results
Scenario(Map<SearchEverywhereContributor<Object>, Integer> contributorsAndLimits, Map<String, List<String>> results) {
this.contributorsAndLimits = contributorsAndLimits
this.results = results
}
}
static class SearchResultsCollector implements SESearcher.Listener {
private final Map<String, List<String>> myMap = new ConcurrentHashMap<>()
private final AtomicBoolean myFinished = new AtomicBoolean(false)
private final Phaser myPhaser = new Phaser(2)
List<String> getContributorValues(String contributorId) {
List<String> 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<? extends SearchEverywhereFoundElementInfo> added) {
added.forEach({ info ->
List<String> list = myMap.computeIfAbsent(info.getContributor().getSearchProviderId(), { s -> new ArrayList<>() })
list.add((String)info.getElement())
})
}
@Override
void elementsRemoved(@NotNull List<? extends SearchEverywhereFoundElementInfo> removed) {
removed.forEach({ info ->
List<String> 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<SearchEverywhereContributor<?>, Boolean> hasMoreContributors) {
hasMoreContributors.entrySet()
.stream()
.filter({ entry -> entry.getValue() })
.map({ entry -> entry.getKey() })
.forEach({ contributor ->
List<String> 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()
}
}
}
@@ -855,6 +855,10 @@
<description>Defines whether to show "What's new" notification after IDE update or not.</description>
</experimentalFeature>
<experimentalFeature id="search.everywhere.mixed.results" internalFeature="true" percentOfUsers="0">
<description>Use mixed list of results on All tab in Search Everywhere (instead of Files/Classes/Actions groups)</description>
</experimentalFeature>
<rawEditorTypedHandler implementationClass="com.intellij.openapi.editor.impl.EditorFactoryImpl$MyRawTypedHandler"/>
<postStartupActivity implementation="com.intellij.diagnostic.AnalyzePendingSnapshotActivity"/>
@@ -165,6 +165,10 @@
<keyboard-shortcut first-keystroke="meta 3"/>
<keyboard-shortcut first-keystroke="meta alt U"/>
</action>
<action id="SearchEverywhere.NavigateToNextGroup">
<keyboard-shortcut first-keystroke="PAGE_DOWN"/>
<keyboard-shortcut first-keystroke="meta DOWN"/>
</action>
<action id="EditorToggleShowWhitespaces"/>
<action id="EditorToggleUseSoftWraps"/>
<action id="Refresh"/>
@@ -181,5 +185,9 @@
<keyboard-shortcut first-keystroke="meta control 4"/>
<keyboard-shortcut first-keystroke="meta alt COMMA"/>
</action>
<action id="SearchEverywhere.NavigateToPrevGroup">
<keyboard-shortcut first-keystroke="PAGE_UP"/>
<keyboard-shortcut first-keystroke="meta UP"/>
</action>
</keymap>