typo tolerant completion

This commit is contained in:
Max Medvedev
2018-06-29 12:39:35 +03:00
parent 51ddf49f94
commit 8afdaeaca3
5 changed files with 1043 additions and 19 deletions
@@ -1,4 +1,4 @@
// Copyright 2000-2017 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.
// 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.codeInsight.completion;
import com.intellij.codeInsight.ExpectedTypeInfo;
@@ -8,6 +8,7 @@ import com.intellij.codeInsight.completion.util.ParenthesesInsertHandler;
import com.intellij.codeInsight.daemon.impl.analysis.LambdaHighlightingUtil;
import com.intellij.codeInsight.lookup.*;
import com.intellij.openapi.util.Conditions;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.patterns.ElementPattern;
import com.intellij.patterns.PsiElementPattern;
import com.intellij.pom.java.LanguageLevel;
@@ -115,7 +116,8 @@ public class JavaKeywordCompletion {
JavaKeywordCompletion(CompletionParameters parameters, JavaCompletionSession session) {
myParameters = parameters;
mySession = session;
myKeywordMatcher = new FixingLayoutPlainMatcher(session.getMatcher().getPrefix());
myKeywordMatcher = Registry.is("ide.completion.typo.tolerance") ? session.getMatcher()
: new FixingLayoutPlainMatcher(session.getMatcher().getPrefix());
myPosition = parameters.getPosition();
myPrevLeaf = prevSignificantLeaf(myPosition);
@@ -657,6 +657,8 @@ ide.completion.max.width.description=Maximum width for completion popup in pixel
ide.java.completion.suggest.static.after.instance=false
ide.java.completion.suggest.static.after.instance.description=Suggest static methods/fields after instance qualifier in Java completion
ide.completion.typo.tolerance=false
ide.goto.middle.matching=true
ide.goto.middle.matching.description=Suggest items in goto actions that contain the entered string somewhere in the middle.
@@ -1632,4 +1634,5 @@ java.folding.icons.for.control.flow.description=Add folding icons to the gutter
show.new.plugin.page=true
show.new.plugin.page.description=Show new plugin page configurable
ide.action.menu.item.pack.on.text.update=true
ide.action.menu.item.pack.on.text.update=true
@@ -0,0 +1,32 @@
// 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.psi.codeStyle;
import com.intellij.openapi.util.TextRange;
import com.intellij.util.containers.FList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class FixingLayoutTypoTolerantMatcher extends TypoTolerantMatcher {
@Nullable
private final MinusculeMatcher myFixedMatcher;
public FixingLayoutTypoTolerantMatcher(@NotNull String pattern, @NotNull NameUtil.MatchingCaseSensitivity options, String hardSeparators) {
super(pattern, options, hardSeparators);
String s = FixingLayoutMatcher.fixLayout(pattern);
myFixedMatcher = s == null ? null : new TypoTolerantMatcher(s, options, hardSeparators);
}
@Override
public boolean matches(@NotNull String name) {
return super.matches(name) || myFixedMatcher != null && myFixedMatcher.matches(name);
}
@Nullable
@Override
public FList<TextRange> matchingFragments(@NotNull String name) {
FList<TextRange> ranges = super.matchingFragments(name);
if (myFixedMatcher == null || ranges != null && !ranges.isEmpty()) return ranges;
return myFixedMatcher.matchingFragments(name);
}
}
@@ -1,20 +1,7 @@
/*
* Copyright 2000-2017 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.
*/
// 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.psi.codeStyle;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
@@ -439,7 +426,8 @@ public class NameUtil {
}
public MinusculeMatcher build() {
return new FixingLayoutMatcher(pattern, caseSensitivity, separators);
return Registry.is("ide.completion.typo.tolerance") ? new FixingLayoutTypoTolerantMatcher(pattern, caseSensitivity, separators)
: new FixingLayoutMatcher(pattern, caseSensitivity, separators);
}
}
@@ -0,0 +1,999 @@
// 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.psi.codeStyle;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.Processor;
import com.intellij.util.SmartList;
import com.intellij.util.containers.FList;
import com.intellij.util.io.IOUtil;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
class TypoTolerantMatcher extends MinusculeMatcher {
private final char[] myPattern;
private final String myHardSeparators;
private final NameUtil.MatchingCaseSensitivity myOptions;
private final boolean myHasHumps;
private final boolean myHasSeparators;
private final boolean myHasDots;
private final boolean[] isLowerCase;
private final boolean[] isUpperCase;
private final boolean[] isWordSeparator;
private final char[] toUpperCase;
private final char[] toLowerCase;
private final char[] myMeaningfulCharacters;
private final int myMinNameLength;
/**
* Constructs a matcher by a given pattern.
* @param pattern the pattern
* @param options case sensitivity settings
* @param hardSeparators A string of characters (empty by default). Lowercase humps don't work for parts separated by any of these characters.
* Need either an explicit uppercase letter or the same separator character in prefix
*/
TypoTolerantMatcher(@NotNull String pattern, @NotNull NameUtil.MatchingCaseSensitivity options, @NotNull String hardSeparators) {
super(pattern, options, hardSeparators);
myOptions = options;
myPattern = StringUtil.trimEnd(pattern, "* ").toCharArray();
myHardSeparators = hardSeparators;
isLowerCase = new boolean[myPattern.length];
isUpperCase = new boolean[myPattern.length];
isWordSeparator = new boolean[myPattern.length];
toUpperCase = new char[myPattern.length];
toLowerCase = new char[myPattern.length];
StringBuilder meaningful = new StringBuilder();
for (int k = 0; k < myPattern.length; k++) {
char c = myPattern[k];
isLowerCase[k] = Character.isLowerCase(c);
isUpperCase[k] = Character.isUpperCase(c);
isWordSeparator[k] = isWordSeparator(c);
toUpperCase[k] = StringUtil.toUpperCase(c);
toLowerCase[k] = StringUtil.toLowerCase(c);
if (!isWildcard(k)) {
meaningful.append(toLowerCase[k]);
meaningful.append(toUpperCase[k]);
}
}
int i = 0;
while (isWildcard(i)) i++;
myHasHumps = hasFlag(i + 1, isUpperCase) && hasFlag(i, isLowerCase);
myHasSeparators = hasFlag(i, isWordSeparator);
myHasDots = hasDots(i);
myMeaningfulCharacters = meaningful.toString().toCharArray();
myMinNameLength = myMeaningfulCharacters.length / 2;
}
private static boolean isWordSeparator(char c) {
return Character.isWhitespace(c) || c == '_' || c == '-' || c == ':' || c == '+' || c == '.';
}
private static int nextWord(@NotNull String name, int start) {
if (start < name.length() && Character.isDigit(name.charAt(start))) {
return start + 1; //treat each digit as a separate hump
}
return NameUtil.nextWord(name, start);
}
private boolean hasFlag(int start, boolean[] flags) {
for (int i = start; i < myPattern.length; i++) {
if (flags[i]) {
return true;
}
}
return false;
}
private boolean hasDots(int start) {
for (int i = start; i < myPattern.length; i++) {
if (myPattern[i] == '.') {
return true;
}
}
return false;
}
@NotNull
private static FList<TextRange> prependRange(@NotNull FList<TextRange> ranges, @NotNull Range range) {
Range head = ((Range)ranges.getHead());
if (head != null && head.getStartOffset() == range.getEndOffset()) {
return ranges.getTail().prepend(new Range(range.getStartOffset(), head.getEndOffset(), range.getErrorCount() + head.getErrorCount()));
}
return ranges.prepend(range);
}
public int matchingDegree(@NotNull String name) {
return matchingDegree(name, false);
}
public int matchingDegree(@NotNull String name, boolean valueStartCaseMatch) {
return matchingDegree(name, valueStartCaseMatch, matchingFragments(name));
}
public int matchingDegree(@NotNull String name, boolean valueStartCaseMatch, @Nullable FList<TextRange> fragments) {
if (fragments == null) return Integer.MIN_VALUE;
if (fragments.isEmpty()) return 0;
final TextRange first = fragments.getHead();
boolean startMatch = first.getStartOffset() == 0;
boolean valuedStartMatch = startMatch && valueStartCaseMatch;
int errors = 0;
int matchingCase = 0;
int p = -1;
int skippedHumps = 0;
int nextHumpStart = 0;
boolean humpStartMatchedUpperCase = false;
for (TextRange range : fragments) {
for (int i = range.getStartOffset(); i < range.getEndOffset(); i++) {
boolean afterGap = i == range.getStartOffset() && first != range;
boolean isHumpStart = false;
while (nextHumpStart <= i) {
if (nextHumpStart == i) {
isHumpStart = true;
}
else if (afterGap) {
skippedHumps++;
}
nextHumpStart = nextWord(name, nextHumpStart);
}
char c = name.charAt(i);
p = StringUtil.indexOf(myPattern, c, p + 1, myPattern.length, false);
if (p < 0) {
break;
}
if (isHumpStart) {
humpStartMatchedUpperCase = c == myPattern[p] && isUpperCase[p];
}
matchingCase += evaluateCaseMatching(valuedStartMatch, p, humpStartMatchedUpperCase, i, afterGap, isHumpStart, c);
}
errors += 2000.0 * Math.pow(1.0 * ((Range)range).getErrorCount() / range.getLength(), 2);
}
int startIndex = first.getStartOffset();
boolean afterSeparator = StringUtil.indexOfAny(name, myHardSeparators, 0, startIndex) >= 0;
boolean wordStart = startIndex == 0 || NameUtil.isWordStart(name, startIndex) && !NameUtil.isWordStart(name, startIndex - 1);
boolean finalMatch = fragments.get(fragments.size() - 1).getEndOffset() == name.length();
return (wordStart ? 1000 : 0) +
matchingCase +
-fragments.size() +
-skippedHumps * 10 +
-errors +
(afterSeparator ? 0 : 2) +
(startMatch ? 1 : 0) +
(finalMatch ? 1 : 0);
}
private int evaluateCaseMatching(boolean valuedStartMatch,
int patternIndex,
boolean humpStartMatchedUpperCase,
int nameIndex,
boolean afterGap,
boolean isHumpStart,
char nameChar) {
if (afterGap && isHumpStart && isLowerCase[patternIndex]) {
return -10; // disprefer when there's a hump but nothing in the pattern indicates the user meant it to be hump
}
if (nameChar == myPattern[patternIndex]) {
if (isUpperCase[patternIndex]) return 50; // strongly prefer user's uppercase matching uppercase: they made an effort to press Shift
if (nameIndex == 0 && valuedStartMatch) return 150; // the very first letter case distinguishes classes in Java etc
if (isHumpStart) return 1; // if a lowercase matches lowercase hump start, that also means something
} else if (isHumpStart) {
// disfavor hump starts where pattern letter case doesn't match name case
return -1;
} else if (isLowerCase[patternIndex] && humpStartMatchedUpperCase) {
// disfavor lowercase non-humps matching uppercase in the name
return -1;
}
return 0;
}
public boolean isStartMatch(@NotNull String name) {
FList<TextRange> fragments = matchingFragments(name);
return fragments != null && isStartMatch(fragments);
}
public static boolean isStartMatch(@NotNull Iterable<TextRange> fragments) {
Iterator<TextRange> iterator = fragments.iterator();
return !iterator.hasNext() || iterator.next().getStartOffset() == 0;
}
@Override
public boolean matches(@NotNull String name) {
return matchingFragments(name) != null;
}
@NotNull
public String getPattern() {
return new String(myPattern);
}
@Nullable
public FList<TextRange> matchingFragments(@NotNull String name) {
FList<TextRange> ranges = new Session(name, false).matchingFragments();
if (ranges != null) return ranges;
return new Session(name, true).matchingFragments();
}
private class Session {
@NotNull private final String myName;
private final boolean isAsciiName;
private final boolean myTypoAware;
private final boolean myAllowTypos;
public Session(@NotNull String name, boolean typoAware) {
myName = name;
int length = name.length();
boolean isAscii = true;
for (int i = 0; i < length; ++i) {
char c = name.charAt(i);
if (c >= 128) {
isAscii = false;
}
}
isAsciiName = isAscii;
myTypoAware = typoAware;
myAllowTypos = typoAware && isAscii;
}
private char charAt(int i, @NotNull ErrorState errorState) {
return errorState.affects(i) ? errorState.getChar(myPattern, i) : myPattern[i];
}
private char toLowerCase(int i, @NotNull ErrorState errorState) {
return errorState.affects(i) ? toLowerAscii(errorState.getChar(myPattern, i)) : toLowerCase[i];
}
private char toUpperCase(int i, @NotNull ErrorState errorState) {
return errorState.affects(i) ? toUpperAscii(errorState.getChar(myPattern, i)) : toUpperCase[i];
}
private boolean isLowerCase(int i, @NotNull ErrorState errorState) {
return errorState.affects(i) ? isLowerAscii(errorState.getChar(myPattern, i)) : isLowerCase[i];
}
private boolean isUpperCase(int i, @NotNull ErrorState errorState) {
return errorState.affects(i) ? isUpperAscii(errorState.getChar(myPattern, i)) : isUpperCase[i];
}
private boolean isWordSeparator(int i, @NotNull ErrorState errorState) {
return errorState.affects(i) ? TypoTolerantMatcher.isWordSeparator(errorState.getChar(myPattern, i)) : isWordSeparator[i];
}
private int patternLength(@NotNull ErrorState errorState) {
return errorState.length(myPattern);
}
@Nullable
public FList<TextRange> matchingFragments() {
if (myName.length() < myMinNameLength) {
return null;
}
//we're in typo mode, but non-ascii symbols are used. so aborting
if (myTypoAware && !isAsciiName) return null;
if (!myTypoAware) {
int length = myName.length();
int patternIndex = 0;
for (int i = 0; i < length; ++i) {
char c = myName.charAt(i);
if (patternIndex < myMeaningfulCharacters.length &&
(c == myMeaningfulCharacters[patternIndex] || c == myMeaningfulCharacters[patternIndex + 1])) {
patternIndex += 2;
}
}
if (patternIndex < myMinNameLength * 2) {
return null;
}
}
return matchWildcards(0, 0, new ErrorState());
}
/**
* After a wildcard (* or space), search for the first non-wildcard pattern character in the name starting from nameIndex
* and try to {@link #matchFragment} for it.
*/
@Nullable
private FList<TextRange> matchWildcards(int patternIndex,
int nameIndex,
@NotNull ErrorState errorState) {
if (nameIndex < 0) {
return null;
}
if (!isWildcard(patternIndex)) {
if (patternIndex == patternLength(errorState)) {
return FList.emptyList();
}
return matchFragment(patternIndex, nameIndex, errorState);
}
do {
patternIndex++;
} while (isWildcard(patternIndex));
if (patternIndex == patternLength(errorState)) {
// the trailing space should match if the pattern ends with the last word part, or only its first hump character
if (isTrailingSpacePattern(errorState) && nameIndex != myName.length() && (patternIndex < 2 || !isUpperCaseOrDigit(charAt(
patternIndex - 2, errorState)))) {
int spaceIndex = myName.indexOf(' ', nameIndex);
if (spaceIndex >= 0) {
return FList.<TextRange>emptyList().prepend(new Range(spaceIndex, spaceIndex + 1, 0));
}
return null;
}
return FList.emptyList();
}
FList<TextRange> ranges = matchFragment(patternIndex, nameIndex, errorState);
if (ranges != null) {
return ranges;
}
return matchSkippingWords(patternIndex, nameIndex, true, errorState);
}
private boolean isTrailingSpacePattern(@NotNull ErrorState errorState) {
return isPatternChar(patternLength(errorState) - 1, ' ', errorState);
}
private boolean isUpperCaseOrDigit(char p) {
return Character.isUpperCase(p) || Character.isDigit(p);
}
/**
* Enumerates places in name that could be matched by the pattern at patternIndex position
* and invokes {@link #matchFragment} at those candidate positions
*/
@Nullable
private FList<TextRange> matchSkippingWords(int patternIndex,
int nameIndex,
boolean allowSpecialChars,
@NotNull ErrorState errorState) {
boolean wordStartsOnly = !isPatternChar(patternIndex - 1, '*', errorState) && !isWordSeparator(patternIndex, errorState);
int maxFoundLength = 0;
while (true) {
nameIndex = findNextPatternCharOccurrence(nameIndex, patternIndex, allowSpecialChars, wordStartsOnly, errorState);
if (nameIndex < 0) {
return null;
}
Fragment fragment = seemsLikeFragmentStart(patternIndex, nameIndex, errorState) ? maxMatchingFragment(patternIndex, nameIndex, errorState) : null;
if (fragment == null) continue;
// match the remaining pattern only if we haven't already seen fragment of the same (or bigger) length
// because otherwise it means that we already tried to match remaining pattern letters after it with the remaining name and failed
// but now we have the same remaining pattern letters and even less remaining name letters, and so will fail as well
int fragmentLength = fragment.getLength();
if (fragmentLength > maxFoundLength || nameIndex + fragmentLength == myName.length() && isTrailingSpacePattern(errorState)) {
if (!isMiddleMatch(patternIndex, nameIndex, errorState)) {
maxFoundLength = fragmentLength;
}
FList<TextRange> ranges = matchInsideFragment(patternIndex, nameIndex, fragment);
if (ranges != null) {
return ranges;
}
}
}
}
private int findNextPatternCharOccurrence(int startAt,
int patternIndex,
boolean allowSpecialChars,
boolean wordStartsOnly,
@NotNull ErrorState errorState) {
int next = wordStartsOnly
? indexOfWordStart(patternIndex, startAt, errorState)
: indexOfIgnoreCase(startAt + 1, patternIndex, errorState);
// pattern humps are allowed to match in words separated by " ()", lowercase characters aren't
if (!allowSpecialChars && !myHasSeparators && !myHasHumps && StringUtil.containsAnyChar(myName, myHardSeparators, startAt, next)) {
return -1;
}
// if the user has typed a dot, don't skip other dots between humps
// but one pattern dot may match several name dots
if (!allowSpecialChars && myHasDots && !isPatternChar(patternIndex - 1, '.', errorState) && StringUtil.contains(myName, startAt, next, '.')) {
return -1;
}
return next;
}
private boolean seemsLikeFragmentStart(int patternIndex, int nextOccurrence, @NotNull ErrorState errorState) {
// uppercase should match either uppercase or a word start
return !isUpperCase(patternIndex, errorState) ||
Character.isUpperCase(myName.charAt(nextOccurrence)) ||
NameUtil.isWordStart(myName, nextOccurrence) ||
// accept uppercase matching lowercase if the whole prefix is uppercase and case sensitivity allows that
!myHasHumps && myOptions != NameUtil.MatchingCaseSensitivity.ALL;
}
private boolean charEquals(int patternIndex, int nameIndex, boolean isIgnoreCase, boolean allowTypos, @NotNull ErrorState errorState) {
char patternChar = charAt(patternIndex, errorState);
char nameChar = myName.charAt(nameIndex);
if (patternChar == nameChar ||
isIgnoreCase && (toLowerCase(patternIndex, errorState) == nameChar || toUpperCase(patternIndex, errorState) == nameChar)) {
return true;
}
if (!myAllowTypos || !allowTypos) return false;
if (errorState.countErrors(0, patternIndex) > 0) return false;
Error prevError = errorState.getError(patternIndex - 1);
if (prevError == SwapError.instance) {
return false;
}
char leftMiss = leftMiss(patternChar);
if (leftMiss != 0) {
if (leftMiss == nameChar ||
isIgnoreCase && (toLowerAscii(leftMiss) == nameChar || toUpperAscii(leftMiss) == nameChar)) {
errorState.addError(patternIndex, new TypoError(leftMiss));
return true;
}
}
char rightMiss = rightMiss(patternChar);
if (rightMiss != 0) {
if (rightMiss == nameChar ||
isIgnoreCase && (toLowerAscii(rightMiss) == nameChar || toUpperAscii(rightMiss) == nameChar)) {
errorState.addError(patternIndex, new TypoError(rightMiss));
return true;
}
}
if (patternLength(errorState) > patternIndex + 1 && myName.length() > nameIndex + 1) {
char nextNameChar = myName.charAt(nameIndex + 1);
char nextPatternChar = charAt(patternIndex + 1, errorState);
if ((patternChar == nextNameChar || isIgnoreCase && (toLowerCase(patternIndex, errorState) == nextNameChar || toUpperCase(patternIndex, errorState) == nextNameChar)) &&
(nextPatternChar == nameChar || isIgnoreCase && (toLowerCase(patternIndex + 1, errorState) == nameChar || toUpperCase(patternIndex + 1, errorState) == nameChar))) {
errorState.addError(patternIndex, SwapError.instance);
return true;
}
}
if (myName.length() > nameIndex + 1) {
char nextNameChar = myName.charAt(nameIndex + 1);
if (patternChar == nextNameChar || isIgnoreCase && (toLowerCase(patternIndex, errorState) == nextNameChar || toUpperCase(patternIndex, errorState) == nextNameChar)) {
errorState.addError(patternIndex, new MissError(nameChar));
return true;
}
}
return false;
}
@Nullable
private FList<TextRange> matchFragment(int patternIndex,
int nameIndex,
@NotNull ErrorState errorState) {
Fragment fragment = maxMatchingFragment(patternIndex, nameIndex, errorState);
return fragment == null ? null : matchInsideFragment(patternIndex, nameIndex, fragment);
}
@Nullable
private Fragment maxMatchingFragment(int patternIndex, int nameIndex, @NotNull ErrorState baseErrorState) {
ErrorState errorState = baseErrorState.deriveFrom(patternIndex);
if (!isFirstCharMatching(nameIndex, patternIndex, errorState)) {
return null;
}
int i = 1;
boolean ignoreCase = myOptions != NameUtil.MatchingCaseSensitivity.ALL;
while (nameIndex + i < myName.length() && patternIndex + i < patternLength(errorState)) {
if (!charEquals(patternIndex + i, nameIndex + i, ignoreCase, true, errorState)) {
if (Character.isDigit(charAt(patternIndex + i, errorState)) && Character.isDigit(charAt(patternIndex + i - 1, errorState))) {
return null;
}
break;
}
if (isUppercasePatternVsLowercaseNameChar(patternIndex + i, nameIndex + i, errorState) &&
shouldProhibitCaseMismatch(patternIndex + i, nameIndex + i, errorState)) {
break;
}
i++;
}
return new Fragment(i, errorState);
}
// we've found the longest fragment matching pattern and name
@Nullable
private FList<TextRange> matchInsideFragment(int patternIndex,
int nameIndex,
@NotNull Fragment fragment) {
// exact middle matches have to be at least of length 3, to prevent too many irrelevant matches
int minFragment = isMiddleMatch(patternIndex, nameIndex, fragment.getErrorState())
? 3 : 1;
FList<TextRange> camelHumpRanges = improveCamelHumps(patternIndex, nameIndex, fragment.getLength(), minFragment, fragment.getErrorState());
if (camelHumpRanges != null) {
return camelHumpRanges;
}
return findLongestMatchingPrefix(patternIndex, nameIndex, fragment.getLength(), minFragment, fragment.getErrorState());
}
private boolean isMiddleMatch(int patternIndex, int nameIndex, @NotNull ErrorState errorState) {
return isPatternChar(patternIndex - 1, '*', errorState) && !isWildcard(patternIndex + 1) &&
Character.isLetterOrDigit(myName.charAt(nameIndex)) && !NameUtil.isWordStart(myName, nameIndex);
}
@Nullable
private FList<TextRange> findLongestMatchingPrefix(int patternIndex,
int nameIndex,
int fragmentLength, int minFragment,
@NotNull ErrorState errorState) {
if (patternIndex + fragmentLength >= patternLength(errorState)) {
int errors = errorState.countErrors(patternIndex, patternIndex + fragmentLength);
if (errors == fragmentLength) return null;
return FList.<TextRange>emptyList().prepend(new Range(nameIndex, nameIndex + fragmentLength, errors));
}
// try to match the remainder of pattern with the remainder of name
// it may not succeed with the longest matching fragment, then try shorter matches
for (int i = fragmentLength; i >= minFragment || isWildcard(patternIndex + i); i--) {
ErrorState derivedErrorState = errorState.deriveFrom(patternIndex + i);
FList<TextRange> ranges = isWildcard(patternIndex + i) ?
matchWildcards(patternIndex + i, nameIndex + i, derivedErrorState) :
matchSkippingWords(patternIndex + i, nameIndex + i, false, derivedErrorState);
if (ranges != null) {
int errors = errorState.countErrors(patternIndex, patternIndex + i);
if (errors == i) return null;
return prependRange(ranges, new Range(nameIndex, nameIndex + i, errors));
}
}
return null;
}
/**
* When pattern is "CU" and the name is "CurrentUser", we already have a prefix "Cu" that matches,
* but we try to find uppercase "U" later in name for better matching degree
*/
@Nullable
private FList<TextRange> improveCamelHumps(int patternIndex,
int nameIndex,
int maxFragment,
int minFragment,
@NotNull ErrorState errorState) {
for (int i = minFragment; i < maxFragment; i++) {
if (isUppercasePatternVsLowercaseNameChar(patternIndex + i, nameIndex + i, errorState)) {
FList<TextRange> ranges = findUppercaseMatchFurther(patternIndex + i, nameIndex + i, errorState.deriveFrom(patternIndex + i));
if (ranges != null) {
int errors = errorState.countErrors(patternIndex, patternIndex + i);
if (errors == i) return null;
return prependRange(ranges, new Range(nameIndex, nameIndex + i, errors));
}
}
}
return null;
}
private boolean isUppercasePatternVsLowercaseNameChar(int patternIndex, int nameIndex, @NotNull ErrorState errorState) {
return isUpperCase(patternIndex, errorState) && !charEquals(patternIndex, nameIndex, false, false, errorState);
}
@Nullable
private FList<TextRange> findUppercaseMatchFurther(int patternIndex,
int nameIndex,
@NotNull ErrorState errorState) {
int nextWordStart = indexOfWordStart(patternIndex, nameIndex, errorState);
return matchWildcards(patternIndex, nextWordStart, errorState.deriveFrom(patternIndex));
}
private boolean shouldProhibitCaseMismatch(int patternIndex, int nameIndex, @NotNull ErrorState errorState) {
// at least three consecutive uppercase letters shouldn't match lowercase
if (myHasHumps && patternIndex >= 2 && isUpperCase(patternIndex - 1, errorState) && isUpperCase(patternIndex - 2, errorState)) {
// but if there's a lowercase after them, it can match (in case shift was released a bit later)
if (nameIndex + 1 == myName.length() ||
patternIndex + 1 < patternLength(errorState) && !isLowerCase(patternIndex + 1, errorState)) {
return true;
}
}
return false;
}
private boolean isFirstCharMatching(int nameIndex, int patternIndex, @NotNull ErrorState errorState) {
if (nameIndex >= myName.length()) return false;
boolean ignoreCase = myOptions != NameUtil.MatchingCaseSensitivity.ALL;
if (!charEquals(patternIndex, nameIndex, ignoreCase, true, errorState)) return false;
char patternChar = charAt(patternIndex, errorState);
if (myOptions == NameUtil.MatchingCaseSensitivity.FIRST_LETTER &&
(patternIndex == 0 || patternIndex == 1 && isWildcard(0)) &&
hasCase(patternChar) &&
Character.isUpperCase(patternChar) != Character.isUpperCase(myName.charAt(0))) {
return false;
}
return true;
}
private boolean hasCase(char patternChar) {
return Character.isUpperCase(patternChar) || Character.isLowerCase(patternChar);
}
private boolean isPatternChar(int patternIndex, char c, @NotNull ErrorState errorState) {
return patternIndex >= 0 && patternIndex < patternLength(errorState) && charAt(patternIndex, errorState) == c;
}
private int indexOfWordStart(int patternIndex, int startFrom, @NotNull ErrorState errorState) {
if (startFrom >= myName.length() ||
myHasHumps && isLowerCase(patternIndex, errorState) && !(patternIndex > 0 && isWordSeparator(patternIndex - 1, errorState))) {
return -1;
}
int nextWordStart = startFrom;
while (true) {
nextWordStart = nextWord(myName, nextWordStart);
if (nextWordStart >= myName.length()) {
return -1;
}
if (charEquals(patternIndex, nextWordStart, true, true, errorState)) {
return nextWordStart;
}
}
}
private int indexOfIgnoreCase(int fromIndex, int patternIndex, @NotNull ErrorState errorState) {
char p = charAt(patternIndex, errorState);
if (isAsciiName && IOUtil.isAscii(p)) {
int i = indexIgnoringCaseAscii(fromIndex, patternIndex, p);
if (i != -1) return i;
if (myAllowTypos) {
int leftMiss = indexIgnoringCaseAscii(fromIndex, patternIndex, leftMiss(p));
if (leftMiss != -1) return leftMiss;
int rightMiss = indexIgnoringCaseAscii(fromIndex, patternIndex, rightMiss(p));
if (rightMiss != -1) return rightMiss;
}
return -1;
}
return StringUtil.indexOfIgnoreCase(myName, p, fromIndex);
}
private int indexIgnoringCaseAscii(int fromIndex, int patternIndex, char p) {
char pUpper = toUpperAscii(p);
char pLower = toLowerAscii(p);
for (int i = fromIndex; i < myName.length(); i++) {
char c = myName.charAt(i);
if (c == p || toUpperAscii(c) == pUpper || toLowerAscii(c) == pLower) {
return i;
}
}
return -1;
}
}
private boolean isWildcard(int patternIndex) {
if (patternIndex >= 0 && patternIndex < myPattern.length) {
char pc = myPattern[patternIndex];
return pc == ' ' || pc == '*';
}
return false;
}
private static char toUpperAscii(char c) {
if (c >= 'a' && c <= 'z') {
return (char)(c + ('A' - 'a'));
}
return c;
}
private static char toLowerAscii(char c) {
if (c >= 'A' && c <= 'Z') {
return (char)(c - ('A' - 'a'));
}
return c;
}
private static boolean isUpperAscii(char c) {
return 'A' <= c && c <= 'Z';
}
private static boolean isLowerAscii(char c) {
return 'a' <= c && c <= 'z';
}
@NonNls
@Override
public String toString() {
return "TypoTolerantMatcher{myPattern=" + new String(myPattern) + ", myOptions=" + myOptions + '}';
}
private static class ErrorState {
@Nullable private final ErrorState myBase;
private final int myDeriveIndex;
private List<Pair<Integer, Error>> myErrors;
private char[] myPattern;
ErrorState(@Nullable ErrorState base, int deriveIndex) {
myBase = base;
myDeriveIndex = deriveIndex;
}
public ErrorState() {
this(null, 0);
}
@NotNull
ErrorState deriveFrom(int index) {
return new ErrorState(this, index);
}
void addError(int index, @NotNull Error error) {
if (myErrors == null) myErrors = new SmartList<Pair<Integer, Error>>();
Pair<Integer, Error> pair = Pair.create(index, error);
myErrors.add(pair);
if (myPattern != null) {
myPattern = applyError(myPattern, pair);
}
}
int countErrors(int start, int end) {
int errors = 0;
if (myBase != null && start < myDeriveIndex) {
errors += myBase.countErrors(start, myDeriveIndex);
}
if (myErrors != null) {
for (Pair<Integer, Error> error : myErrors) {
if (start <= error.first && error.first < end) {
errors++;
}
}
}
return errors;
}
private boolean processErrors(int start, int end, Processor<Pair<Integer, Error>> processor) {
if (myBase != null && start < myDeriveIndex) {
if (!myBase.processErrors(start, myDeriveIndex, processor)) {
return false;
}
}
if (myErrors != null) {
for (Pair<Integer, Error> error : myErrors) {
if (start <= error.first && error.first < end) {
if (!processor.process(error)) {
return false;
}
}
}
}
return true;
}
public char getChar(char[] pattern, int index) {
if (myPattern == null) {
myPattern = applyErrors(Arrays.copyOf(pattern, pattern.length), Integer.MAX_VALUE);
}
return myPattern[index];
}
private char[] applyErrors(char[] pattern, int upToIndex) {
if (myBase != null) {
pattern = myBase.applyErrors(pattern, Math.min(myDeriveIndex, upToIndex));
}
if (myErrors != null) {
for (Pair<Integer, Error> error : myErrors) {
if (error.first < upToIndex) {
pattern = applyError(pattern, error);
}
}
}
return pattern;
}
private static char[] applyError(char[] pattern, Pair<Integer, Error> error) {
if (error.second instanceof TypoError) {
pattern[error.first] = ((TypoError)error.second).myCorrectChar;
return pattern;
}
else if (error.second instanceof SwapError) {
char c = pattern[error.first];
pattern[error.first] = pattern[error.first + 1];
pattern[error.first + 1] = c;
return pattern;
}
else if (error.second instanceof MissError) {
return insert(pattern, error.first, ((MissError)error.second).myMissedChar);
}
return pattern;
}
public boolean affects(final int index) {
return !processErrors(0, index + 1, new Processor<Pair<Integer, Error>>() {
@Override
public boolean process(Pair<Integer, Error> error) {
if (error.first == index) return false;
if (error.first == index - 1 && error.second == SwapError.instance) return false;
if (error.first < index) {
if (error.second instanceof MissError) return false;
//todo support extra
}
return true;
}
});
}
public Error getError(int i) {
if (myErrors != null) {
for (Pair<Integer, Error> error : myErrors) {
if (error.first == i) return error.second;
}
}
if (myBase != null && myDeriveIndex > i) {
return myBase.getError(i);
}
return null;
}
public int length(char[] pattern) {
final Ref<Integer> ref = new Ref<Integer>(pattern.length);
processErrors(0, Integer.MAX_VALUE, new Processor<Pair<Integer, Error>>() {
@Override
public boolean process(Pair<Integer, Error> error) {
if (error.second instanceof MissError) {
ref.set(ref.get() + 1);
}
return true;
}
});
return ref.get();
}
}
private interface Error { }
private static class TypoError implements Error {
private final char myCorrectChar;
public TypoError(char correctChar) {
myCorrectChar = correctChar;
}
}
private static class SwapError implements Error {
public static final SwapError instance = new SwapError();
}
private static class MissError implements Error {
private final char myMissedChar;
public MissError(char missedChar) {
myMissedChar = missedChar;
}
}
private static class Fragment {
private final int myLength;
private final ErrorState myErrorState;
public Fragment(int length, @NotNull ErrorState errorState) {
myLength = length;
myErrorState = errorState;
}
int getLength() {return myLength;}
@NotNull
ErrorState getErrorState() {return myErrorState;}
}
private static class Range extends TextRange {
private final int myErrorCount;
public Range(int startOffset, int endOffset, int errorCount) {
super(startOffset, endOffset);
myErrorCount = errorCount;
}
public int getErrorCount() {
return myErrorCount;
}
@NotNull
public Range shiftRight(int delta) {
if (delta == 0) return this;
return new Range(getStartOffset() + delta, getEndOffset() + delta, getErrorCount());
}
public static Range from(int from, int length) {
return new Range(from, from + length, 0);
}
}
@NotNull
@Contract(pure = true)
public static char[] insert(@NotNull char[] array, int index, char value) {
char[] result = new char[array.length + 1];
System.arraycopy(array, 0, result, 0, index);
result[index] = value;
System.arraycopy(array, index, result, index + 1, array.length - index);
return result;
}
private static final char[][] keyboard = {
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'},
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'},
{'z', 'x', 'c', 'v', 'b', 'n', 'm'}
};
private static char leftMiss(char aChar) {
boolean isUpperCase = isUpperAscii(aChar);
char lc = isUpperCase ? toLowerAscii(aChar) : aChar;
for (char[] line : keyboard) {
for (int j = 0; j < line.length; j++) {
char c = line[j];
if (c == lc) {
if (j > 0) {
return isUpperCase ? toUpperAscii(line[j - 1]) : line[j - 1];
}
else {
return 0;
}
}
}
}
return 0;
}
private static char rightMiss(char aChar) {
boolean isUpperCase = isUpperAscii(aChar);
char lc = isUpperCase ? toLowerAscii(aChar) : aChar;
for (char[] line : keyboard) {
for (int j = 0; j < line.length; j++) {
char c = line[j];
if (c == lc) {
if (j + 1 < line.length) {
return isUpperCase ? toUpperAscii(line[j + 1]) : line[j + 1];
}
else {
return 0;
}
}
}
}
return 0;
}
}