/*
* Copyright 2000-2009 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.workbench.goTo.matcher;
import com.intellij.concurrency.JobLauncher;
import com.intellij.ide.util.gotoByName.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.codeStyle.MinusculeMatcher;
import com.intellij.psi.codeStyle.NameUtil;
import com.intellij.psi.search.EverythingGlobalScope;
import com.intellij.psi.util.proximity.PsiProximityComparator;
import com.intellij.util.Function;
import com.intellij.util.Processor;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.indexing.FindSymbolParameters;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
/**
* This is copied from com.intellij.ide.util.gotoByName.DefaultChooseByNameItemProvider, the change is
* that this one considers the whole pattern as a name (see http://youtrack.jetbrains.com/issue/MPS-16902)
*/
public class MPSNodeItemProvider implements ChooseByNameItemProvider {
private static final Logger LOG = Logger.getInstance("#com.intellij.ide.util.gotoByName.ChooseByNameIdea");
private WeakReference<PsiElement> myContext;
public MPSNodeItemProvider(PsiElement context) {
myContext = new WeakReference<PsiElement>(context);
}
@Override
public boolean filterElements(@NotNull ChooseByNameBase base,
@NotNull String pattern,
boolean everywhere,
@NotNull ProgressIndicator indicator,
@NotNull Processor<Object> consumer) {
//change beginning
String namePattern = pattern;
String qualifierPattern = "";
//change end
ChooseByNameModel model = base.getModel();
boolean empty = namePattern.isEmpty() ||
namePattern.equals("@") && model instanceof GotoClassModel2; // TODO[yole]: remove implicit dependency
if (empty && !base.canShowListForEmptyPattern()) return true;
Set<String> names = new THashSet<String>(Arrays.asList(base.getNames(everywhere)));
if (base.isSearchInAnyPlace() && !namePattern.trim().isEmpty()) {
String middleMatchPattern = "*" + namePattern + (namePattern.endsWith(" ") ? "" : "*");
// consume elements matching by prefix case-sensitively
Integer elementsConsumed = consumeElements(base, everywhere, indicator, consumer, namePattern, qualifierPattern, names,
NameUtil.MatchingCaseSensitivity.ALL, false);
if (elementsConsumed == null) return false;
if (elementsConsumed == 0) {
// search with original pattern without case sensitivity, don't add separator before found items
// result: items matched by prefix will always be above middle-matched items
elementsConsumed = consumeElements(base, everywhere, indicator, consumer, namePattern,
qualifierPattern, names, NameUtil.MatchingCaseSensitivity.NONE, false);
if (elementsConsumed == null) return false;
}
// search with broadest criteria - middle match pattern, without case sensitivity
elementsConsumed = consumeElements(base, everywhere, indicator, consumer, middleMatchPattern,
qualifierPattern, names, NameUtil.MatchingCaseSensitivity.NONE, elementsConsumed > 0);
return elementsConsumed != null;
}
else {
Integer elementsConsumed = consumeElements(base, everywhere, indicator, consumer, namePattern, qualifierPattern, names,
NameUtil.MatchingCaseSensitivity.NONE, false);
return elementsConsumed != null;
}
}
/**
* @return null if consumer returned false, number of consumed elements otherwise.
*/
private Integer consumeElements(ChooseByNameBase base,
boolean everywhere,
ProgressIndicator indicator,
Processor<Object> consumer,
String namePattern,
String qualifierPattern,
Set<String> allNames,
NameUtil.MatchingCaseSensitivity sensitivity,
boolean needSeparator) {
ChooseByNameModel model = base.getModel();
List<String> namesList = new ArrayList<String>();
getNamesByPattern(base, new ArrayList<String>(allNames), indicator, namesList, namePattern, sensitivity);
allNames.removeAll(namesList);
sortNamesList(namePattern, namesList);
indicator.checkCanceled();
List<Object> sameNameElements = new SmartList<Object>();
List<Pair<String, MinusculeMatcher>> patternsAndMatchers = getPatternsAndMatchers(qualifierPattern, base);
int elementsConsumed = 0;
for (String name : namesList) {
indicator.checkCanceled();
//TODO: use FindSymbolParameters.wrap(namePattern, ???, everywhere)
final FindSymbolParameters findSymbolParameters = new FindSymbolParameters(namePattern, namePattern, new EverythingGlobalScope(), null);
// use interruptible call if possible
Object[] elements = model instanceof ContributorsBasedGotoByModel ?
((ContributorsBasedGotoByModel)model).getElementsByName(name, findSymbolParameters, indicator)
: model.getElementsByName(name, everywhere, namePattern);
if (elements.length > 1) {
sameNameElements.clear();
for (final Object element : elements) {
indicator.checkCanceled();
if (matchesQualifier(element, base, patternsAndMatchers)) {
sameNameElements.add(element);
}
}
sortByProximity(base, sameNameElements);
for (Object element : sameNameElements) {
if (needSeparator && !consumer.process(ChooseByNameBase.NON_PREFIX_SEPARATOR)) return null;
if (!consumer.process(element)) return null;
needSeparator = false;
elementsConsumed++;
}
}
else if (elements.length == 1 && matchesQualifier(elements[0], base, patternsAndMatchers)) {
if (needSeparator && !consumer.process(ChooseByNameBase.NON_PREFIX_SEPARATOR)) return null;
if (!consumer.process(elements[0])) return null;
needSeparator = false;
elementsConsumed++;
}
}
return elementsConsumed;
}
protected void sortNamesList(@NotNull String namePattern, List<String> namesList) {
// Here we sort using namePattern to have similar logic with empty qualified patten case
Collections.sort(namesList, new MatchesComparator(namePattern));
}
private void sortByProximity(@NotNull ChooseByNameBase base, final List<Object> sameNameElements) {
final ChooseByNameModel model = base.getModel();
if (model instanceof Comparator) {
//noinspection unchecked
Collections.sort(sameNameElements, (Comparator)model);
} else {
Collections.sort(sameNameElements, new PathProximityComparator(model, myContext.get()));
}
}
private static String getQualifierPattern(@NotNull ChooseByNameBase base, @NotNull String pattern) {
final String[] separators = base.getModel().getSeparators();
int lastSeparatorOccurrence = 0;
for (String separator : separators) {
lastSeparatorOccurrence = Math.max(lastSeparatorOccurrence, pattern.lastIndexOf(separator));
}
return pattern.substring(0, lastSeparatorOccurrence);
}
public static String getNamePattern(@NotNull ChooseByNameBase base, String pattern) {
pattern = base.transformPattern(pattern);
ChooseByNameModel model = base.getModel();
final String[] separators = model.getSeparators();
int lastSeparatorOccurrence = 0;
for (String separator : separators) {
final int idx = pattern.lastIndexOf(separator);
lastSeparatorOccurrence = Math.max(lastSeparatorOccurrence, idx == -1 ? idx : idx + separator.length());
}
return pattern.substring(lastSeparatorOccurrence);
}
@NotNull
private static List<String> split(@NotNull String s, @NotNull ChooseByNameBase base) {
List<String> answer = new ArrayList<String>();
for (String token : StringUtil.tokenize(s, StringUtil.join(base.getModel().getSeparators(), ""))) {
if (!token.isEmpty()) {
answer.add(token);
}
}
return answer.isEmpty() ? Collections.singletonList(s) : answer;
}
private static boolean matchesQualifier(final Object element,
@NotNull final ChooseByNameBase base,
@NotNull List<Pair<String, MinusculeMatcher>> patternsAndMatchers) {
final String name = base.getModel().getFullName(element);
if (name == null) return false;
final List<String> suspects = split(name, base);
try {
int matchPosition = 0;
patterns:
for (Pair<String, MinusculeMatcher> patternAndMatcher : patternsAndMatchers) {
final String pattern = patternAndMatcher.first;
final MinusculeMatcher matcher = patternAndMatcher.second;
if (!pattern.isEmpty()) {
for (int j = matchPosition; j < suspects.size() - 1; j++) {
String suspect = suspects.get(j);
if (matches(base, pattern, matcher, suspect)) {
matchPosition = j + 1;
continue patterns;
}
}
return false;
}
}
}
catch (Exception e) {
// Do nothing. No matches appears valid result for "bad" pattern
return false;
}
return true;
}
@NotNull
private static List<Pair<String, MinusculeMatcher>> getPatternsAndMatchers(String qualifierPattern, final ChooseByNameBase base) {
return ContainerUtil.map2List(split(qualifierPattern, base), new Function<String, Pair<String, MinusculeMatcher>>() {
@NotNull
@Override
public Pair<String, MinusculeMatcher> fun(String s) {
return Pair.create(getNamePattern(base, s), buildPatternMatcher(getNamePattern(base, s), NameUtil.MatchingCaseSensitivity.NONE));
}
});
}
@NotNull
@Override
public List<String> filterNames(@NotNull ChooseByNameBase base, @NotNull String[] names, @NotNull String pattern) {
List<String> res = new ArrayList<String>();
getNamesByPattern(base, Arrays.asList(names), null, res, pattern, NameUtil.MatchingCaseSensitivity.NONE);
return res;
}
private static void getNamesByPattern(@NotNull final ChooseByNameBase base,
@NotNull List<String> names,
@Nullable ProgressIndicator indicator,
@NotNull final List<String> list,
@NotNull String pattern,
@NotNull NameUtil.MatchingCaseSensitivity caseSensitivity) throws ProcessCanceledException {
if (!base.canShowListForEmptyPattern()) {
LOG.assertTrue(!pattern.isEmpty(), base);
}
if (StringUtil.startsWithChar(pattern, '@') && base.getModel() instanceof GotoClassModel2) {
pattern = pattern.substring(1);
}
final MinusculeMatcher matcher = buildPatternMatcher(pattern, caseSensitivity);
final String finalPattern = pattern;
JobLauncher.getInstance().invokeConcurrentlyUnderProgress(names, indicator, false, new Processor<String>() {
@Override
public boolean process(String name) {
if (matches(base, finalPattern, matcher, name)) {
synchronized (list) {
list.add(name);
}
}
return true;
}
});
}
private static boolean matches(@NotNull ChooseByNameBase base,
@NotNull String pattern,
@NotNull MinusculeMatcher matcher,
@Nullable String name) {
if (name == null) {
return false;
}
boolean matches = false;
if (base.getModel() instanceof CustomMatcherModel) {
if (((CustomMatcherModel)base.getModel()).matches(name, pattern)) {
matches = true;
}
}
else if (pattern.isEmpty() || matcher.matches(name)) {
matches = true;
}
return matches;
}
private static MinusculeMatcher buildPatternMatcher(@NotNull String pattern, @NotNull NameUtil.MatchingCaseSensitivity caseSensitivity) {
return NameUtil.buildMatcher(pattern, caseSensitivity);
}
private static class MatchesComparator implements Comparator<String> {
private final String myOriginalPattern;
private MatchesComparator(@NotNull final String originalPattern) {
myOriginalPattern = originalPattern.trim();
}
@Override
public int compare(@NotNull final String a, @NotNull final String b) {
boolean aStarts = a.startsWith(myOriginalPattern);
boolean bStarts = b.startsWith(myOriginalPattern);
if (aStarts && bStarts) return a.compareToIgnoreCase(b);
if (aStarts && !bStarts) return -1;
if (bStarts && !aStarts) return 1;
return a.compareToIgnoreCase(b);
}
}
private static class PathProximityComparator implements Comparator<Object> {
private final ChooseByNameModel myModel;
@NotNull private final PsiProximityComparator myProximityComparator;
private PathProximityComparator(final ChooseByNameModel model, @Nullable final PsiElement context) {
myModel = model;
myProximityComparator = new PsiProximityComparator(context);
}
@Override
public int compare(final Object o1, final Object o2) {
int rc = myProximityComparator.compare(o1, o2);
if (rc != 0) return rc;
return Comparing.compare(myModel.getFullName(o1), myModel.getFullName(o2));
}
}
}