/*
* 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.ide.util.gotoByName.ChooseByNameBase;
import com.intellij.ide.util.gotoByName.ChooseByNameItemProvider;
import com.intellij.ide.util.gotoByName.ChooseByNameModel;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.codeStyle.MinusculeMatcher;
import com.intellij.psi.codeStyle.NameUtil;
import com.intellij.psi.codeStyle.NameUtil.MatchingCaseSensitivity;
import com.intellij.util.Processor;
import com.intellij.util.containers.FList;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
public class MPSPackageItemProvider implements ChooseByNameItemProvider {
@Override
public boolean filterElements(@NotNull ChooseByNameBase base, @NotNull String pattern, boolean everywhere, @NotNull ProgressIndicator indicator,
@NotNull Processor<Object> consumer) {
return filterElements(base.getNames(everywhere), pattern, everywhere, base.getModel(), indicator, consumer);
}
// This method is here for testing purposes only.
boolean filterElements(@NotNull String[] names, @NotNull String pattern, boolean everywhere, @NotNull ChooseByNameModel model,
@NotNull ProgressIndicator indicator, @NotNull Processor<Object> consumer) {
MinusculeMatcher matcher = NameUtil.buildMatcher(adjustMatchingPattern(pattern), MatchingCaseSensitivity.NONE);
List<MatchingResult> matchingResults = new ArrayList<>();
for (String name : names) {
List<TextRange> nameSegments = getNameSegments(name, getSeparators(model));
if (!nameSegments.isEmpty()) {
indicator.checkCanceled();
MatchingResult matchingResult = new MatchingResult(name, nameSegments.get(nameSegments.size() - 1), matcher);
if (matchingResult.wasMatched()) {
matchingResults.add(matchingResult);
continue;
}
}
indicator.checkCanceled();
MatchingResult matchingResult = new MatchingResult(name, nameSegments, matcher);
if (matchingResult.wasMatched()) {
matchingResults.add(matchingResult);
}
}
Collections.sort(matchingResults);
for (MatchingResult matchingResult : matchingResults) {
indicator.checkCanceled();
if (!consumer.process(model.getElementsByName(matchingResult.getName(), everywhere, pattern)[0])) {
return false;
}
}
return true;
}
private String adjustMatchingPattern(String pattern) {
// adding "*" in the beginning of the pattern, to convert entered string to the IDEA Matcher pattern
// (IDEA Matcher is expecting exact start pattern matching)
return pattern.startsWith("*") ? pattern : "*" + pattern;
}
private String getSeparators(ChooseByNameModel model) {
StringBuilder allSeparators = new StringBuilder();
for (String nextSeparator : model.getSeparators()) {
allSeparators.append(nextSeparator);
}
return allSeparators.toString();
}
private List<TextRange> getNameSegments(String name, String separators) {
List<TextRange> result = new ArrayList<>();
StringTokenizer tokenizer = new StringTokenizer(name, separators, false);
int lastIndex = 0;
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
int segmentStart = name.indexOf(token, lastIndex);
int segmentEnd = segmentStart + token.length();
result.add(new TextRange(segmentStart, segmentEnd));
lastIndex = segmentEnd;
}
return result;
}
@NotNull
@Override
public List<String> filterNames(@NotNull ChooseByNameBase base, @NotNull String[] names, @NotNull String pattern) {
return Collections.emptyList();
}
private class MatchingResult implements Comparable<MatchingResult> {
private final String myName;
private int myMatchingFragmentsCount;
private int myCompletelyMatchedFragments;
private int myFragmentStartMatches;
private int myMatchingDegree;
private boolean myMatched;
private boolean myShortNameMatch;
private MatchingResult(String name, TextRange shortNameSegment, MinusculeMatcher matcher) {
myName = name;
myShortNameMatch = true;
match(shortNameSegment.substring(name), Collections.singletonList(shortNameSegment), matcher);
}
private MatchingResult(String name, List<TextRange> nameSegments, MinusculeMatcher matcher) {
myName = name;
match(name, nameSegments, matcher);
}
private void match(String nameToMatch, List<TextRange> nameSegments, MinusculeMatcher matcher) {
FList<TextRange> matchingFragments = matcher.matchingFragments(nameToMatch);
myMatched = matchingFragments != null;
if (!myMatched) {
return;
}
myMatchingDegree = matcher.matchingDegree(nameToMatch, true, matchingFragments);
myMatchingFragmentsCount = matchingFragments.size();
int nameIndex = 0;
int matchingIndex = 0;
while (nameIndex < nameSegments.size() && matchingIndex < matchingFragments.size()) {
TextRange nameSegment = nameSegments.get(nameIndex);
TextRange matchingFragment = matchingFragments.get(matchingIndex);
// Name segment may be less than matching fragment - matching fragment can include more symbols: separator char and
// start of the following name segment.
// Complete matching criteria is: name fragment should be completely included into the matching fragment
if (nameSegment.getStartOffset() == matchingFragment.getStartOffset() && nameSegment.getEndOffset() <= matchingFragment.getEndOffset()) {
myCompletelyMatchedFragments++;
}
if (nameSegment.getStartOffset() == matchingFragment.getStartOffset()) {
myFragmentStartMatches++;
nameIndex++;
matchingIndex++;
} else if (nameSegment.getStartOffset() < matchingFragment.getStartOffset()) {
nameIndex++;
} else {
matchingIndex++;
}
}
}
public String getName() {
return myName;
}
public boolean wasMatched() {
return myMatched;
}
@Override
public int compareTo(@NotNull MatchingResult o) {
if (myShortNameMatch != o.myShortNameMatch) {
return myShortNameMatch ? -1 : 1;
}
if (myMatchingFragmentsCount != o.myMatchingFragmentsCount) {
return myMatchingFragmentsCount - o.myMatchingFragmentsCount;
}
if (myCompletelyMatchedFragments != o.myCompletelyMatchedFragments) {
return o.myCompletelyMatchedFragments - myCompletelyMatchedFragments;
}
if (myFragmentStartMatches != o.myFragmentStartMatches) {
return o.myFragmentStartMatches - myFragmentStartMatches;
}
if (myMatchingDegree != o.myMatchingDegree) {
return o.myMatchingDegree - myMatchingDegree;
}
return myName.compareTo(o.myName);
}
}
}