blob: 9886aefd5cd4176a90af1109be46ab65f7335648 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2020 Julian Honnen.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Julian Honnen - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.core.compiler;
import java.util.Arrays;
import java.util.BitSet;
import org.eclipse.jdt.internal.compiler.parser.ScannerHelper;
class SubwordMatcher {
private static final int[] EMPTY_REGIONS = new int[0];
private final char[] name;
private final BitSet wordBoundaries;
public SubwordMatcher(String name) {
this.name = name.toCharArray();
this.wordBoundaries = new BitSet(name.length());
for (int i = 0; i < this.name.length; i++) {
if (isWordBoundary(caseAt(i - 1), caseAt(i), caseAt(i + 1))) {
this.wordBoundaries.set(i);
}
}
}
private Case caseAt(int index) {
if (index < 0 || index >= this.name.length)
return Case.SEPARATOR;
char c = this.name[index];
if (c == '_')
return Case.SEPARATOR;
if (ScannerHelper.isUpperCase(c))
return Case.UPPER;
return Case.LOWER;
}
private static boolean isWordBoundary(Case p, Case c, Case n) {
if (p == c && c == n)
return false; // a boundary needs some kind of gradient
if (p == Case.SEPARATOR)
return true; // boundary after every separator
// the remaining cases are boundaries for capitalization changes:
// lowerUpper, UPPERLower, lowerUPPER
// ^ ^ ^
return (c == Case.UPPER) && (p == Case.LOWER || n == Case.LOWER);
}
private enum Case {
SEPARATOR, LOWER, UPPER
}
public int[] getMatchingRegions(String pattern) {
int segmentStart = 0;
int[] segments = EMPTY_REGIONS;
// Main loop is on pattern characters
int iName = -1;
int iPatternWordStart = 0;
for (int iPattern = 0; iPattern < pattern.length(); iPattern++) {
iName++;
if (iName == this.name.length) {
// We have exhausted the name (and not the pattern), so it's not a match
return null;
}
char patternChar = pattern.charAt(iPattern);
char nameChar = this.name[iName];
// For as long as we're exactly matching, bring it on
if (patternChar == nameChar) {
continue;
}
if (!isWordBoundary(iName) && equalsIgnoreCase(patternChar, nameChar)) {
// we're not at a word boundary, case-insensitive match is fine
continue;
}
// not matching, record previous segment and find next word match in name
if (iName > segmentStart) {
segments = Arrays.copyOf(segments, segments.length + 2);
segments[segments.length - 2] = segmentStart;
segments[segments.length - 1] = iName - segmentStart;
}
int wordStart = indexOfWordStart(iName, patternChar);
if (wordStart < 0) {
// no matching word found, backtrack and try to find next occurrence of current word
int next = indexOfWordStart(iName, pattern.charAt(iPatternWordStart));
if (next > 0) {
wordStart = next;
iPattern = iPatternWordStart;
// last recorded segment was invalid -> drop it
segments = Arrays.copyOfRange(segments, 0, segments.length - 2);
}
}
if (wordStart < 0) {
// We have exhausted name (and not pattern), so it's not a match
return null;
}
segmentStart = wordStart;
iName = wordStart;
iPatternWordStart = iPattern;
}
// we have exhausted pattern, record final segment
segments = Arrays.copyOf(segments, segments.length + 2);
segments[segments.length - 2] = segmentStart;
segments[segments.length - 1] = iName - segmentStart + 1;
return segments;
}
/**
* Returns the index of the first word after nameStart, beginning with patternChar. Returns -1 if no matching word
* is found.
*/
private int indexOfWordStart(int nameStart, char patternChar) {
for (int iName = nameStart; iName < this.name.length; iName++) {
char nameChar = this.name[iName];
if (isWordBoundary(iName) && equalsIgnoreCase(nameChar, patternChar)) {
return iName;
}
// don't match across identifiers (e.g. "index" should not match "substring(int beginIndex)")
if (!ScannerHelper.isJavaIdentifierPart(nameChar)) {
return -1;
}
}
// We have exhausted name
return -1;
}
private boolean equalsIgnoreCase(char a, char b) {
return ScannerHelper.toLowerCase(a) == ScannerHelper.toLowerCase(b);
}
private boolean isWordBoundary(int iName) {
return this.wordBoundaries.get(iName);
}
}