/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.filters; import com.google.gwt.regexp.shared.MatchResult; import com.google.gwt.regexp.shared.RegExp; import com.google.inject.Singleton; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.eclipse.che.ide.filters.Matcher.MatcherUtil.or; /** * @author Evgen Vidolob */ @Singleton public class FuzzyMatches { private final Matcher FUZZY_SEPARATE = or(new PrefixMatcher(true), new CamelCaseMatcher(), new SubStringMatcher()); private final Matcher FUZZY_CONTIGUOUS = or(new PrefixMatcher(true), new CamelCaseMatcher(), new ContiguousSubStringMatcher()); private Map<String, RegExp> regExpCache = new HashMap<>(); public List<Match> fuzzyMatch(String word, String wordToMatch) { return fuzzyMatch(word, wordToMatch, false); } public List<Match> fuzzyMatch(String word, String wordToMatch, boolean substringMatch) { RegExp regExp = regExpCache.get(word); if (regExp == null) { regExp = convertWordToRegExp(word); regExpCache.put(word, regExp); } MatchResult matchResult = regExp.exec(wordToMatch); if (matchResult != null) { return Collections.singletonList(new Match(matchResult.getIndex(), matchResult.getIndex() + matchResult.getGroup(0).length())); } if (substringMatch) { return FUZZY_SEPARATE.match(word, wordToMatch); } else { return FUZZY_CONTIGUOUS.match(word, wordToMatch); } } private boolean isUpper(int code) { return 65 <= code && code <= 90; } private boolean isLower(int code) { return 97 <= code && code <= 122; } private boolean isWhitespace(int code) { return code != 32 && code != 9 && code != 10 && code != 13; } private boolean isAlphanumeric(int code) { return isLower(code) || isUpper(code) || isNumber(code); } private boolean isNumber(int code) { return 48 <= code && code <= 57; } private List<Match> join(Match match, List<Match> result) { if (result.isEmpty()) { result.add(match); } else if (match.getEnd() == result.get(0).getStart()) { result.get(0).setStart(match.getStart()); } else { result.add(0, match); } return result; } private RegExp convertWordToRegExp(String word) { return RegExp.compile(createRegexpNative(word), "i"); } private native String createRegexpNative(String word) /*-{ return word.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*'); }-*/; private class ContiguousSubStringMatcher implements Matcher { @Override public List<Match> match(String word, String wordToMatch) { int index = wordToMatch.toLowerCase().indexOf(word.toLowerCase()); if (index == -1) { return null; } return Collections.singletonList(new Match(index, index + word.length() + 1)); } } private class SubStringMatcher implements Matcher { @Override public List<Match> match(String word, String wordToMatch) { return matchSubString(word.toLowerCase(), wordToMatch.toLowerCase(), 0, 0); } private List<Match> matchSubString(String word, String wodToMatch, int i, int j) { if (i == word.length()) { return new ArrayList<>(); } else if (j == wodToMatch.length()) { return null; } else { if (word.charAt(i) == wodToMatch.charAt(j)) { List<Match> result; if ((result = matchSubString(word, wodToMatch, i + 1, j + 1)) != null) { return join(new Match(j, j + 1), result); } } return matchSubString(word, wodToMatch, i, j + 1); } } } private class CamelCaseMatcher implements Matcher { @Override public List<Match> match(String word, String wordToMatch) { if (wordToMatch == null || wordToMatch.isEmpty()) { return null; } if (!isCamelCasePattern(word)) { return null; } if (!isCamelCaseWord(wordToMatch)) { return null; } List<Match> result = null; int i = 0; while (i < wordToMatch.length() && (result = matchCamelCase(word.toLowerCase(), wordToMatch, 0, i)) == null) { i = nextAnchor(wordToMatch, i + 1); } return result; } private List<Match> matchCamelCase(String word, String wordToMatch, int i, int j) { if (i == word.length()) { return new ArrayList<>(); } else if (j == wordToMatch.length()) { return null; } else if (word.charAt(i) != wordToMatch.toLowerCase().charAt(j)) { return null; } else { List<Match> result = null; int nextUpperIndex = j + 1; result = matchCamelCase(word, wordToMatch, i + 1, j + 1); while (result == null && (nextUpperIndex = nextAnchor(wordToMatch, nextUpperIndex)) < wordToMatch.length()) { result = matchCamelCase(word, wordToMatch, i + 1, nextUpperIndex); nextUpperIndex++; } return result == null ? null : join(new Match(j, j + 1), result); } } private int nextAnchor(String wordToMatch, int start) { for (int i = start; i < wordToMatch.length(); i++) { int c = wordToMatch.charAt(i); if (isUpper(c) || isNumber(c) || (i > 0 && !isAlphanumeric(wordToMatch.charAt(i - 1)))) { return i; } } return wordToMatch.length(); } private boolean isCamelCaseWord(String word) { if (word.length() > 60) { return false; } double upper = 0; double lower = 0; double alpha = 0; double numeric = 0; int code = 0; for (int i = 0; i < word.length(); i++) { code = word.charAt(i); if (isUpper(code)) { upper++; } if (isLower(code)) { lower++; } if (isAlphanumeric(code)) { alpha++; } if (isNumber(code)) { numeric++; } } double upperPercent = upper / word.length(); double lowerPercent = lower / word.length(); double alphaPercent = alpha / word.length(); double numericPercent = numeric / word.length(); return lowerPercent > 0.2d && upperPercent < 0.8d && alphaPercent > 0.6d && numericPercent < 0.5d; } private boolean isCamelCasePattern(String word) { int upper = 0; int lower = 0; int code; int whitespace = 0; for (int i = 0; i < word.length(); i++) { code = word.charAt(i); if (isUpper(code)) { upper++; } if (isLower(code)) { lower++; } if (isWhitespace(code)) { whitespace++; } } if ((upper == 0 || lower == 0) && whitespace == 0) { return word.length() <= 30; } else { return upper <= 5; } } } private class PrefixMatcher implements Matcher { private final boolean ignoreCase; public PrefixMatcher(boolean ignoreCase) { this.ignoreCase = ignoreCase; } @Override public List<Match> match(String word, String wordToMatch) { if (wordToMatch == null || wordToMatch.isEmpty() || wordToMatch.length() < word.length()) { return null; } if (ignoreCase) { word = word.toLowerCase(); wordToMatch = wordToMatch.toLowerCase(); } for (int i = 0; i < word.length(); i++) { if (word.charAt(i) != wordToMatch.charAt(i)) { return null; } } return word.length() > 0 ? Collections.singletonList(new Match(0, word.length())) : Collections.<Match>emptyList(); } } }