Bug 516383 - Support substring matching for completion proposal
Change-Id: If7910ac2d5282bdfeba6e6d11bb09e5ce7ca743c
Signed-off-by: Alex Xu <ibazzi@qq.com>
diff --git a/core/plugins/org.eclipse.dltk.core/compiler/org/eclipse/dltk/compiler/CharOperation.java b/core/plugins/org.eclipse.dltk.core/compiler/org/eclipse/dltk/compiler/CharOperation.java
index 386ee82..bfdfb4a 100644
--- a/core/plugins/org.eclipse.dltk.core/compiler/org/eclipse/dltk/compiler/CharOperation.java
+++ b/core/plugins/org.eclipse.dltk.core/compiler/org/eclipse/dltk/compiler/CharOperation.java
@@ -4151,6 +4151,100 @@
}
/**
+ * Answers true if the characters of the pattern are contained in the name
+ * as a substring, in a case-insensitive way.
+ *
+ * @param pattern
+ * the given pattern
+ * @param name
+ * the given name
+ * @return true if the pattern matches the given name, false otherwise
+ * @since 3.12
+ */
+ public static final boolean substringMatch(String pattern, String name) {
+ if (pattern == null || pattern.length() == 0) {
+ return true;
+ }
+ if (name == null) {
+ return false;
+ }
+ return checkSubstringMatch(pattern.toCharArray(), name.toCharArray());
+ }
+
+ /**
+ * Answers true if the characters of the pattern are contained in the name
+ * as a substring, in a case-insensitive way.
+ *
+ * @param pattern
+ * the given pattern
+ * @param name
+ * the given name
+ * @return true if the pattern matches the given name, false otherwise
+ * @since 3.12
+ */
+ public static final boolean substringMatch(char[] pattern, char[] name) {
+ if (pattern == null || pattern.length == 0) {
+ return true;
+ }
+ if (name == null) {
+ return false;
+ }
+ return checkSubstringMatch(pattern, name);
+ }
+
+ /**
+ * Internal substring matching method; called after the null and length
+ * checks are performed.
+ *
+ * @param pattern
+ * the given pattern
+ * @param name
+ * the given name
+ * @return true if the pattern matches the given name, false otherwise
+ *
+ * @see CharOperation#substringMatch(char[], char[])
+ */
+ private static final boolean checkSubstringMatch(char[] pattern,
+ char[] name) {
+
+ /*
+ * XXX: to be revised/enabled
+ *
+ * // allow non-consecutive occurrence of pattern characters if
+ * (pattern.length >= 3) { int pidx = 0;
+ *
+ * for (int nidx = 0; nidx < name.length; nidx++) { if
+ * (Character.toLowerCase(name[nidx]) ==
+ * Character.toLowerCase(pattern[pidx])) pidx++; if (pidx ==
+ * pattern.length) return true; }
+ *
+ * // for short patterns only allow consecutive occurrence } else {
+ */
+ // outer loop iterates on the characters of the name; trying to
+ // match at any possible position
+ outer: for (int nidx = 0; nidx < name.length - pattern.length
+ + 1; nidx++) {
+ // inner loop iterates on pattern characters
+ for (int pidx = 0; pidx < pattern.length; pidx++) {
+ if (Character.toLowerCase(name[nidx + pidx]) != Character
+ .toLowerCase(pattern[pidx])) {
+ // no match until parameter list; do not match parameter
+ // list
+ if ((name[nidx + pidx] == '(')
+ || (name[nidx + pidx] == ':'))
+ return false;
+ continue outer;
+ }
+ if (pidx == pattern.length - 1)
+ return true;
+ }
+ }
+ // XXX: }
+
+ return false;
+ }
+
+ /**
* Answers a new array removing a given character. Answers the given array
* if there is no occurence of the character to remove. <br>
* <br>
diff --git a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/core/DLTKCore.java b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/core/DLTKCore.java
index dd4368b..7215d18 100644
--- a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/core/DLTKCore.java
+++ b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/core/DLTKCore.java
@@ -244,6 +244,27 @@
*/
public static final String CODEASSIST_CAMEL_CASE_MATCH = PLUGIN_ID
+ ".codeComplete.camelCaseMatch"; //$NON-NLS-1$
+
+ /**
+ * Code assist option ID: Activate Substring Code Completion.
+ * <p>
+ * When enabled, completion shows proposals in which the pattern can be
+ * found as a substring in a case-insensitive way.
+ * </p>
+ * <dl>
+ * <dt>Option id:</dt>
+ * <dd><code>"org.eclipse.jdt.core.codeComplete.substringMatch"</code></dd>
+ * <dt>Possible values:</dt>
+ * <dd><code>{ "enabled", "disabled" }</code></dd>
+ * <dt>Default:</dt>
+ * <dd><code>"enabled"</code></dd>
+ * </dl>
+ *
+ * @category CodeAssistOptionID
+ */
+ public static final String CODEASSIST_SUBSTRING_MATCH = PLUGIN_ID
+ + ".codeComplete.substringMatch"; //$NON-NLS-1$
+
/**
* Possible configurable option ID.public static final boolean DEBUG_PARSER
* = false;
diff --git a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/DLTKCorePreferenceInitializer.java b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/DLTKCorePreferenceInitializer.java
index 7247ad7..c710047 100644
--- a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/DLTKCorePreferenceInitializer.java
+++ b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/DLTKCorePreferenceInitializer.java
@@ -47,6 +47,8 @@
defaultOptionsMap.put(DLTKCore.BUILDER_ENABLED, DLTKCore.ENABLED);
defaultOptionsMap.put(DLTKCore.CODEASSIST_CAMEL_CASE_MATCH,
DLTKCore.ENABLED);
+ defaultOptionsMap.put(DLTKCore.CODEASSIST_SUBSTRING_MATCH,
+ DLTKCore.ENABLED);
// encoding setting comes from resource plug-in
optionNames.add(DLTKCore.CORE_ENCODING);
diff --git a/core/plugins/org.eclipse.dltk.core/search/org/eclipse/dltk/core/search/SearchPattern.java b/core/plugins/org.eclipse.dltk.core/search/org/eclipse/dltk/core/search/SearchPattern.java
index 045e80b..49432fb 100644
--- a/core/plugins/org.eclipse.dltk.core/search/org/eclipse/dltk/core/search/SearchPattern.java
+++ b/core/plugins/org.eclipse.dltk.core/search/org/eclipse/dltk/core/search/SearchPattern.java
@@ -28,6 +28,7 @@
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.search.indexing.IIndexConstants;
import org.eclipse.dltk.core.search.matching.MatchLocator;
+import org.eclipse.dltk.internal.core.search.StringOperation;
import org.eclipse.dltk.internal.core.search.matching.FieldPattern;
import org.eclipse.dltk.internal.core.search.matching.InternalSearchPattern;
import org.eclipse.dltk.internal.core.search.matching.LocalVariablePattern;
@@ -190,6 +191,54 @@
*
*/
public static final int R_CAMELCASE_MATCH = 0x0080;
+
+ /**
+ * Match rule: The search pattern contains a Camel Case expression with a
+ * strict expected number of parts. <br>
+ * Examples:
+ * <ul>
+ * <li>'HM' type string pattern will match 'HashMap' and 'HtmlMapper' types,
+ * but not 'HashMapEntry'</li>
+ * <li>'HMap' type string pattern will still match previous 'HashMap' and
+ * 'HtmlMapper' types, but not 'HighMagnitude'</li>
+ * </ul>
+ *
+ * This rule is not intended to be combined with any other match rule. In
+ * case of other match rule flags are combined with this one, then match
+ * rule validation will return a modified rule in order to perform a better
+ * appropriate search request (see {@link #validateMatchRule(String, int)}
+ * for more details).
+ * <p>
+ *
+ * @see CharOperation#camelCaseMatch(char[], char[], boolean) for a detailed
+ * explanation of Camel Case matching.
+ * <p>
+ */
+ public static final int R_CAMELCASE_SAME_PART_COUNT_MATCH = 0x0100;
+
+ /**
+ * Match rule: The search pattern contains a substring expression in a
+ * case-insensitive way.
+ * <p>
+ * Examples:
+ * <ul>
+ * <li>'bar' string pattern will match 'bar1', 'Bar' and 'removeBar'
+ * types,</li>
+ * </ul>
+ *
+ * This rule is not intended to be combined with any other match rule. In
+ * case of other match rule flags are combined with this one, then match
+ * rule validation will return a modified rule in order to perform a better
+ * appropriate search request (see {@link #validateMatchRule(String, int)}
+ * for more details).
+ *
+ * <p>
+ * This is implemented only for code assist and not available for normal
+ * search.
+ *
+ */
+ public static final int R_SUBSTRING_MATCH = 0x0200;
+
private static final int MODE_MASK = R_EXACT_MATCH | R_PREFIX_MATCH
| R_PATTERN_MATCH | R_REGEXP_MATCH;
private int matchRule;
@@ -1525,6 +1574,81 @@
return matchRule;
}
+ public static final int[] getMatchingRegions(String pattern, String name,
+ int matchRule) {
+ if (name == null)
+ return null;
+ final int nameLength = name.length();
+ if (pattern == null) {
+ return new int[] { 0, nameLength };
+ }
+ final int patternLength = pattern.length();
+ boolean countMatch = false;
+ switch (matchRule) {
+ case SearchPattern.R_EXACT_MATCH:
+ if (patternLength == nameLength && pattern.equalsIgnoreCase(name)) {
+ return new int[] { 0, patternLength };
+ }
+ break;
+ case SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE:
+ if (patternLength == nameLength && pattern.equals(name)) {
+ return new int[] { 0, patternLength };
+ }
+ break;
+ case SearchPattern.R_PREFIX_MATCH:
+ if (patternLength <= nameLength && name.substring(0, patternLength)
+ .equalsIgnoreCase(pattern)) {
+ return new int[] { 0, patternLength };
+ }
+ break;
+ case SearchPattern.R_PREFIX_MATCH | SearchPattern.R_CASE_SENSITIVE:
+ if (name.startsWith(pattern)) {
+ return new int[] { 0, patternLength };
+ }
+ break;
+ case SearchPattern.R_CAMELCASE_SAME_PART_COUNT_MATCH:
+ countMatch = true;
+ // $FALL-THROUGH$
+ case SearchPattern.R_CAMELCASE_MATCH:
+ if (patternLength <= nameLength) {
+ int[] regions = StringOperation.getCamelCaseMatchingRegions(
+ pattern, 0, patternLength, name, 0, nameLength,
+ countMatch);
+ if (regions != null)
+ return regions;
+ if (name.substring(0, patternLength)
+ .equalsIgnoreCase(pattern)) {
+ return new int[] { 0, patternLength };
+ }
+ }
+ break;
+ case SearchPattern.R_CAMELCASE_SAME_PART_COUNT_MATCH
+ | SearchPattern.R_CASE_SENSITIVE:
+ countMatch = true;
+ // $FALL-THROUGH$
+ case SearchPattern.R_CAMELCASE_MATCH | SearchPattern.R_CASE_SENSITIVE:
+ if (patternLength <= nameLength) {
+ return StringOperation.getCamelCaseMatchingRegions(pattern, 0,
+ patternLength, name, 0, nameLength, countMatch);
+ }
+ break;
+ case SearchPattern.R_PATTERN_MATCH:
+ return StringOperation.getPatternMatchingRegions(pattern, 0,
+ patternLength, name, 0, nameLength, false);
+ case SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE:
+ return StringOperation.getPatternMatchingRegions(pattern, 0,
+ patternLength, name, 0, nameLength, true);
+ case SearchPattern.R_SUBSTRING_MATCH:
+ if (patternLength <= nameLength) {
+ int next = CharOperation.indexOf(pattern.toCharArray(),
+ name.toCharArray(), false);
+ return next >= 0 ? new int[] { next, patternLength } : null;
+ }
+ break;
+ }
+ return null;
+ }
+
@Override
public String toString() {
return "SearchPattern"; //$NON-NLS-1$
diff --git a/core/plugins/org.eclipse.dltk.core/search/org/eclipse/dltk/internal/core/search/StringOperation.java b/core/plugins/org.eclipse.dltk.core/search/org/eclipse/dltk/internal/core/search/StringOperation.java
new file mode 100644
index 0000000..6555e28
--- /dev/null
+++ b/core/plugins/org.eclipse.dltk.core/search/org/eclipse/dltk/internal/core/search/StringOperation.java
@@ -0,0 +1,560 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Alex Xu and others.
+ * 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:
+ * Alex Xu - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.dltk.internal.core.search;
+
+import org.eclipse.dltk.compiler.CharOperation;
+import org.eclipse.dltk.compiler.util.ScannerHelper;
+
+/**
+ * This class is a collection of helper methods to manipulate strings during
+ * search.
+ */
+public final class StringOperation {
+
+ private final static int[] EMPTY_REGIONS = new int[0];
+
+ /**
+ * Answers all the regions in a given name matching a given camel case
+ * pattern.
+ * </p>
+ * <p>
+ * Each of these regions is made of its starting index and its length in the
+ * given name. They are all concatenated in a single array of
+ * <code>int</code> which therefore always has an even length.
+ * </p>
+ * <p>
+ * Note that each region is disjointed from the following one.<br>
+ * E.g. if the regions are
+ * <code>{ start1, length1, start2, length2 }</code>, then
+ * <code>start1+length1</code> will always be smaller than
+ * <code>start2</code>.
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * Examples:
+ * <ol><li> pattern = "NPE"
+ * name = NullPointerException / NoPermissionException
+ * result: { 0, 1, 4, 1, 11, 1 } / { 0, 1, 2, 1, 12, 1 } </li>
+ * <li> pattern = "NuPoEx"
+ * name = NullPointerException
+ * result: { 0, 2, 4, 2, 11, 2 }</li>
+ * <li> pattern = "IPL3"
+ * name = "IPerspectiveListener3"
+ * result: { 0, 2, 12, 1, 20, 1 }</li>
+ * <li> pattern = "HashME"
+ * name = "HashMapEntry"
+ * result: { 0, 5, 7, 1 }</li>
+ * </ol>
+ * </pre>
+ * </p>
+ *
+ * @see CharOperation#camelCaseMatch(char[], int, int, char[], int, int,
+ * boolean) for more details on the camel case behavior
+ * @see CharOperation#match(char[], char[], boolean) for more details on the
+ * pattern match behavior
+ *
+ * @param pattern
+ * the given pattern
+ * @param patternStart
+ * the start index of the pattern, inclusive
+ * @param patternEnd
+ * the end index of the pattern, exclusive
+ * @param name
+ * the given name
+ * @param nameStart
+ * the start index of the name, inclusive
+ * @param nameEnd
+ * the end index of the name, exclusive
+ * @param samePartCount
+ * flag telling whether the pattern and the name should have the
+ * same count of parts or not.<br>
+ * For example:
+ * <ul>
+ * <li>'HM' type string pattern will match 'HashMap' and
+ * 'HtmlMapper' types, but not 'HashMapEntry'</li>
+ * <li>'HMap' type string pattern will still match previous
+ * 'HashMap' and 'HtmlMapper' types, but not 'HighMagnitude'</li>
+ * </ul>
+ * @return an array of <code>int</code> having two slots per returned
+ * regions (first one is the starting index of the region and the
+ * second one the length of the region).<br>
+ * Note that it may be <code>null</code> if the given name does not
+ * match the pattern
+ */
+ public static final int[] getCamelCaseMatchingRegions(String pattern,
+ int patternStart, int patternEnd, String name, int nameStart,
+ int nameEnd, boolean samePartCount) {
+
+ /*
+ * !!!!!!!!!! WARNING !!!!!!!!!! The algorithm used in this method has
+ * been fully inspired from CharOperation#camelCaseMatch(char[], int,
+ * int, char[], int, int, boolean).
+ *
+ * So, if any change needs to be applied in the algorithm, do NOT forget
+ * to backport it in the CharOperation method!
+ */
+
+ if (name == null)
+ return null; // null name cannot match
+ if (pattern == null) {
+ // null pattern cannot match any region
+ // see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=264816
+ return EMPTY_REGIONS;
+ }
+ if (patternEnd < 0)
+ patternEnd = pattern.length();
+ if (nameEnd < 0)
+ nameEnd = name.length();
+
+ if (patternEnd <= patternStart) {
+ return nameEnd <= nameStart
+ ? new int[] { patternStart, patternEnd - patternStart }
+ : null;
+ }
+ if (nameEnd <= nameStart)
+ return null;
+ // check first pattern char
+ if (name.charAt(nameStart) != pattern.charAt(patternStart)) {
+ // first char must strictly match (upper/lower)
+ return null;
+ }
+
+ char patternChar, nameChar;
+ int iPattern = patternStart;
+ int iName = nameStart;
+
+ // init segments
+ int parts = 1;
+ for (int i = patternStart + 1; i < patternEnd; i++) {
+ final char ch = pattern.charAt(i);
+ if (ch < ScannerHelper.MAX_OBVIOUS) {
+ if ((ScannerHelper.OBVIOUS_IDENT_CHAR_NATURES[ch]
+ & (ScannerHelper.C_UPPER_LETTER
+ | ScannerHelper.C_DIGIT)) != 0) {
+ parts++;
+ }
+ } else if (Character.isJavaIdentifierPart(ch)
+ && (Character.isUpperCase(ch) || Character.isDigit(ch))) {
+ parts++;
+ }
+ }
+ int[] segments = null;
+ int count = 0; // count
+
+ // Main loop is on pattern characters
+ int segmentStart = iName;
+ while (true) {
+ iPattern++;
+ iName++;
+
+ if (iPattern == patternEnd) { // we have exhausted pattern...
+ // it's a match if the name can have additional parts (i.e.
+ // uppercase characters) or is also exhausted
+ if (!samePartCount || iName == nameEnd) {
+ if (segments == null) {
+ segments = new int[2];
+ }
+ segments[count++] = segmentStart;
+ segments[count++] = iName - segmentStart;
+ if (count < segments.length) {
+ System.arraycopy(segments, 0, segments = new int[count],
+ 0, count);
+ }
+ return segments;
+ }
+
+ // otherwise it's a match only if the name has no more uppercase
+ // characters
+ int segmentEnd = iName;
+ while (true) {
+ if (iName == nameEnd) {
+ // we have exhausted the name, so it's a match
+ if (segments == null) {
+ segments = new int[2];
+ }
+ segments[count++] = segmentStart;
+ segments[count++] = segmentEnd - segmentStart;
+ if (count < segments.length) {
+ System.arraycopy(segments, 0,
+ segments = new int[count], 0, count);
+ }
+ return segments;
+ }
+ nameChar = name.charAt(iName);
+ // test if the name character is uppercase
+ if (nameChar < ScannerHelper.MAX_OBVIOUS) {
+ if ((ScannerHelper.OBVIOUS_IDENT_CHAR_NATURES[nameChar]
+ & ScannerHelper.C_UPPER_LETTER) != 0) {
+ return null;
+ }
+ } else if (!Character.isJavaIdentifierPart(nameChar)
+ || Character.isUpperCase(nameChar)) {
+ return null;
+ }
+ iName++;
+ }
+ }
+
+ if (iName == nameEnd) {
+ // We have exhausted the name (and not the pattern), so it's not
+ // a match
+ return null;
+ }
+
+ // For as long as we're exactly matching, bring it on (even if it's
+ // a lower case character)
+ if ((patternChar = pattern.charAt(iPattern)) == name
+ .charAt(iName)) {
+ continue;
+ }
+ int segmentEnd = iName;
+
+ // If characters are not equals, then it's not a match if
+ // patternChar is lowercase
+ if (patternChar < ScannerHelper.MAX_OBVIOUS) {
+ if ((ScannerHelper.OBVIOUS_IDENT_CHAR_NATURES[patternChar]
+ & (ScannerHelper.C_UPPER_LETTER
+ | ScannerHelper.C_DIGIT)) == 0) {
+ return null;
+ }
+ } else if (Character.isJavaIdentifierPart(patternChar)
+ && !Character.isUpperCase(patternChar)
+ && !Character.isDigit(patternChar)) {
+ return null;
+ }
+
+ // patternChar is uppercase, so let's find the next uppercase in
+ // name
+ while (true) {
+ if (iName == nameEnd) {
+ // We have exhausted name (and not pattern), so it's not a
+ // match
+ return null;
+ }
+
+ nameChar = name.charAt(iName);
+ if (nameChar < ScannerHelper.MAX_OBVIOUS) {
+ int charNature = ScannerHelper.OBVIOUS_IDENT_CHAR_NATURES[nameChar];
+ if ((charNature & (ScannerHelper.C_LOWER_LETTER
+ | ScannerHelper.C_SPECIAL)) != 0) {
+ // nameChar is lowercase
+ iName++;
+ } else if ((charNature & ScannerHelper.C_DIGIT) != 0) {
+ // nameChar is digit => break if the digit is current
+ // pattern character otherwise consume it
+ if (patternChar == nameChar)
+ break;
+ iName++;
+ // nameChar is uppercase...
+ } else if (patternChar != nameChar) {
+ // .. and it does not match patternChar, so it's not a
+ // match
+ return null;
+ } else {
+ // .. and it matched patternChar. Back to the big loop
+ break;
+ }
+ }
+ // Same tests for non-obvious characters
+ else if (Character.isJavaIdentifierPart(nameChar)
+ && !Character.isUpperCase(nameChar)) {
+ iName++;
+ } else if (Character.isDigit(nameChar)) {
+ if (patternChar == nameChar)
+ break;
+ iName++;
+ } else if (patternChar != nameChar) {
+ return null;
+ } else {
+ break;
+ }
+ }
+ // At this point, either name has been exhausted, or it is at an
+ // uppercase letter.
+ // Since pattern is also at an uppercase letter
+ if (segments == null) {
+ segments = new int[parts * 2];
+ }
+ segments[count++] = segmentStart;
+ segments[count++] = segmentEnd - segmentStart;
+ segmentStart = iName;
+ }
+ }
+
+ /**
+ * Answers all the regions in a given name matching a given <i>pattern</i>
+ * pattern (e.g. "H*M??").
+ * </p>
+ * <p>
+ * Each of these regions is made of its starting index and its length in the
+ * given name. They are all concatenated in a single array of
+ * <code>int</code> which therefore always has an even length.
+ * </p>
+ * <p>
+ * Note that each region is disjointed from the following one.<br>
+ * E.g. if the regions are
+ * <code>{ start1, length1, start2, length2 }</code>, then
+ * <code>start1+length1</code> will always be smaller than
+ * <code>start2</code>.
+ * </p>
+ * <p>
+ *
+ * <pre>
+ * Examples:
+ * <ol>
+ * <li> pattern = "N???Po*Ex?eption"
+ * name = NullPointerException
+ * result: { 0, 1, 4, 2, 11, 2, 14, 6 }</li>
+ * <li> pattern = "Ha*M*ent*"
+ * name = "HashMapEntry"
+ * result: { 0, 2, 4, 1, 7, 3 }</li>
+ * </ol>
+ * </pre>
+ * </p>
+ *
+ * @see CharOperation#match(char[], char[], boolean) for more details on the
+ * pattern match behavior
+ *
+ * @param pattern
+ * the given pattern
+ * @param patternStart
+ * the given pattern start
+ * @param patternEnd
+ * the given pattern end
+ * @param name
+ * the given name
+ * @param nameStart
+ * the given name start
+ * @param nameEnd
+ * the given name end
+ * @param isCaseSensitive
+ * flag to know if the matching should be case sensitive
+ * @return an array of <code>int</code> having two slots per returned
+ * regions (first one is the starting index of the region and the
+ * second one the length of the region).<br>
+ * Note that it may be <code>null</code> if the given name does not
+ * match the pattern
+ */
+ public static final int[] getPatternMatchingRegions(String pattern,
+ int patternStart, int patternEnd, String name, int nameStart,
+ int nameEnd, boolean isCaseSensitive) {
+
+ /*
+ * !!!!!!!!!! WARNING !!!!!!!!!! The algorithm used in this method has
+ * been fully inspired from CharOperation#match(char[], int, int,
+ * char[], int, int, boolean).
+ *
+ * So, if any change needs to be applied in the algorithm, do NOT forget
+ * to backport it in the CharOperation method!
+ */
+
+ if (name == null)
+ return null; // null name cannot match
+ if (pattern == null) {
+ // null pattern cannot match any region
+ // see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=264816
+ return EMPTY_REGIONS;
+ }
+ int iPattern = patternStart;
+ int iName = nameStart;
+
+ // init segments parts
+ if (patternEnd < 0)
+ patternEnd = pattern.length();
+ if (nameEnd < 0)
+ nameEnd = name.length();
+ int questions = 0;
+ int parts = 0;
+ char previous = 0;
+ for (int i = patternStart; i < patternEnd; i++) {
+ char ch = pattern.charAt(i);
+ switch (ch) {
+ case '?':
+ questions++;
+ break;
+ case '*':
+ break;
+ default:
+ switch (previous) {
+ case 0:
+ case '?':
+ case '*':
+ parts++;
+ break;
+ }
+ }
+ previous = ch;
+ }
+ if (parts == 0) {
+ if (questions <= (nameEnd - nameStart))
+ return EMPTY_REGIONS;
+ return null;
+ }
+ int[] segments = new int[parts * 2];
+
+ /* check first segment */
+ int count = 0;
+ int start = iName;
+ char patternChar = 0;
+ previous = 0;
+ while ((iPattern < patternEnd)
+ && (patternChar = pattern.charAt(iPattern)) != '*') {
+ if (iName == nameEnd)
+ return null;
+ if (patternChar == '?') {
+ switch (previous) {
+ case 0:
+ case '?':
+ break;
+ default:
+ segments[count++] = start;
+ segments[count++] = iPattern - start;
+ break;
+ }
+ } else {
+ if (isCaseSensitive) {
+ if (patternChar != name.charAt(iName)) {
+ return null;
+ }
+ } else if (ScannerHelper
+ .toLowerCase(patternChar) != ScannerHelper
+ .toLowerCase(name.charAt(iName))) {
+ return null;
+ }
+ switch (previous) {
+ case 0:
+ case '?':
+ start = iPattern;
+ break;
+ }
+ }
+ iName++;
+ iPattern++;
+ previous = patternChar;
+ }
+ /* check sequence of star+segment */
+ int segmentStart;
+ if (patternChar == '*') {
+ if (iPattern > 0 && previous != '?') {
+ segments[count++] = start;
+ segments[count++] = iName - start;
+ start = iName;
+ }
+ segmentStart = ++iPattern; // skip star
+ } else {
+ if (iName == nameEnd) {
+ if (count == (parts * 2))
+ return segments;
+ int end = patternEnd;
+ if (previous == '?') { // last char was a '?' => purge all
+ // trailing '?'
+ while (pattern.charAt(--end - 1) == '?') {
+ if (end == start) {
+ return new int[] { patternStart,
+ patternEnd - patternStart };
+ }
+ }
+ }
+ return new int[] { start, end - start };
+ }
+ return null;
+ }
+ int prefixStart = iName;
+ int previousCount = count;
+ previous = patternChar;
+ char previousSegment = patternChar;
+ checkSegment: while (iName < nameEnd) {
+ if (iPattern == patternEnd) {
+ iPattern = segmentStart; // mismatch - restart current segment
+ iName = ++prefixStart;
+ previous = previousSegment;
+ continue checkSegment;
+ }
+ /* segment is ending */
+ if ((patternChar = pattern.charAt(iPattern)) == '*') {
+ segmentStart = ++iPattern; // skip star
+ if (segmentStart == patternEnd) {
+ if (count < (parts * 2)) {
+ segments[count++] = start;
+ segments[count++] = iName - start;
+ }
+ return segments;
+ }
+ switch (previous) {
+ case '*':
+ case '?':
+ break;
+ default:
+ segments[count++] = start;
+ segments[count++] = iName - start;
+ break;
+ }
+ prefixStart = iName;
+ start = prefixStart;
+ previous = patternChar;
+ previousSegment = patternChar;
+ continue checkSegment;
+ }
+ /* check current name character */
+ previousCount = count;
+ if (patternChar == '?') {
+ switch (previous) {
+ case '*':
+ case '?':
+ break;
+ default:
+ segments[count++] = start;
+ segments[count++] = iName - start;
+ break;
+ }
+ } else {
+ boolean mismatch;
+ if (isCaseSensitive) {
+ mismatch = name.charAt(iName) != patternChar;
+ } else {
+ mismatch = ScannerHelper
+ .toLowerCase(name.charAt(iName)) != ScannerHelper
+ .toLowerCase(patternChar);
+ }
+ if (mismatch) {
+ iPattern = segmentStart; // mismatch - restart current
+ // segment
+ iName = ++prefixStart;
+ start = prefixStart;
+ count = previousCount;
+ previous = previousSegment;
+ continue checkSegment;
+ }
+ switch (previous) {
+ case '?':
+ start = iName;
+ break;
+ }
+ }
+ iName++;
+ iPattern++;
+ previous = patternChar;
+ }
+
+ if ((segmentStart == patternEnd)
+ || (iName == nameEnd && iPattern == patternEnd)
+ || (iPattern == patternEnd - 1
+ && pattern.charAt(iPattern) == '*')) {
+ if (count < (parts * 2)) {
+ segments[count++] = start;
+ segments[count++] = iName - start;
+ }
+ return segments;
+ }
+ return null;
+ }
+}
diff --git a/core/plugins/org.eclipse.dltk.ui/core refactoring/org/eclipse/dltk/internal/corext/util/Strings.java b/core/plugins/org.eclipse.dltk.ui/core refactoring/org/eclipse/dltk/internal/corext/util/Strings.java
index 0fbe608..27074cc 100644
--- a/core/plugins/org.eclipse.dltk.ui/core refactoring/org/eclipse/dltk/internal/corext/util/Strings.java
+++ b/core/plugins/org.eclipse.dltk.ui/core refactoring/org/eclipse/dltk/internal/corext/util/Strings.java
@@ -5,7 +5,7 @@
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
-
+
*******************************************************************************/
package org.eclipse.dltk.internal.corext.util;
@@ -14,50 +14,60 @@
import org.eclipse.jface.text.DefaultLineTracker;
import org.eclipse.jface.text.ILineTracker;
import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.viewers.StyledString;
+import org.eclipse.jface.viewers.StyledString.Styler;
/**
- * Helper class to provide String manipulation functions not available in standard JDK.
+ * Helper class to provide String manipulation functions not available in
+ * standard JDK.
*/
public class Strings {
-
- private Strings(){}
-
+
+ private Strings() {
+ }
+
public static boolean startsWithIgnoreCase(String text, String prefix) {
- int textLength= text.length();
- int prefixLength= prefix.length();
+ int textLength = text.length();
+ int prefixLength = prefix.length();
if (textLength < prefixLength)
return false;
- for (int i= prefixLength - 1; i >= 0; i--) {
- if (Character.toLowerCase(prefix.charAt(i)) != Character.toLowerCase(text.charAt(i)))
+ for (int i = prefixLength - 1; i >= 0; i--) {
+ if (Character.toLowerCase(prefix.charAt(i)) != Character
+ .toLowerCase(text.charAt(i)))
return false;
}
return true;
}
+
public static boolean isLowerCase(char ch) {
return Character.toLowerCase(ch) == ch;
}
+
public static String removeMnemonicIndicator(String string) {
return LegacyActionTools.removeMnemonics(string);
}
+
public static String[] convertIntoLines(String input) {
try {
- ILineTracker tracker= new DefaultLineTracker();
+ ILineTracker tracker = new DefaultLineTracker();
tracker.set(input);
- int size= tracker.getNumberOfLines();
- String result[]= new String[size];
- for (int i= 0; i < size; i++) {
- IRegion region= tracker.getLineInformation(i);
- int offset= region.getOffset();
- result[i]= input.substring(offset, offset + region.getLength());
+ int size = tracker.getNumberOfLines();
+ String result[] = new String[size];
+ for (int i = 0; i < size; i++) {
+ IRegion region = tracker.getLineInformation(i);
+ int offset = region.getOffset();
+ result[i] = input.substring(offset,
+ offset + region.getLength());
}
return result;
} catch (BadLocationException e) {
return null;
}
}
+
public static String concatenate(String[] lines, String delimiter) {
- StringBuffer buffer= new StringBuffer();
- for (int i= 0; i < lines.length; i++) {
+ StringBuffer buffer = new StringBuffer();
+ for (int i = 0; i < lines.length; i++) {
if (i > 0)
buffer.append(delimiter);
buffer.append(lines[i]);
@@ -73,4 +83,42 @@
}
return true;
}
+
+ /**
+ * Sets the given <code>styler</code> to use for
+ * <code>matchingRegions</code> (obtained from
+ * {@link org.eclipse.jdt.core.search.SearchPattern#getMatchingRegions}) in
+ * the <code>styledString</code> starting from the given <code>index</code>.
+ *
+ * @param styledString
+ * the styled string to mark
+ * @param index
+ * the index from which to start marking
+ * @param matchingRegions
+ * the regions to mark
+ * @param styler
+ * the styler to use for marking
+ */
+ public static void markMatchingRegions(StyledString styledString, int index,
+ int[] matchingRegions, Styler styler) {
+ if (matchingRegions != null) {
+ int offset = -1;
+ int length = 0;
+ for (int i = 0; i + 1 < matchingRegions.length; i = i + 2) {
+ if (offset == -1)
+ offset = index + matchingRegions[i];
+
+ // Concatenate adjacent regions
+ if (i + 2 < matchingRegions.length && matchingRegions[i]
+ + matchingRegions[i + 1] == matchingRegions[i + 2]) {
+ length = length + matchingRegions[i + 1];
+ } else {
+ styledString.setStyle(offset,
+ length + matchingRegions[i + 1], styler);
+ offset = -1;
+ length = 0;
+ }
+ }
+ }
+ }
}
diff --git a/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/text/completion/AbstractScriptCompletionProposal.java b/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/text/completion/AbstractScriptCompletionProposal.java
index c91914b..a527364 100644
--- a/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/text/completion/AbstractScriptCompletionProposal.java
+++ b/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/text/completion/AbstractScriptCompletionProposal.java
@@ -16,12 +16,16 @@
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.Status;
import org.eclipse.dltk.compiler.CharOperation;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.ModelException;
+import org.eclipse.dltk.core.search.SearchPattern;
+import org.eclipse.dltk.internal.corext.util.Strings;
import org.eclipse.dltk.internal.ui.text.hover.DocumentationHover;
import org.eclipse.dltk.ui.DLTKUIPlugin;
import org.eclipse.dltk.ui.PreferenceConstants;
@@ -43,11 +47,13 @@
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
+import org.eclipse.jface.text.contentassist.BoldStylerProvider;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension5;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension6;
+import org.eclipse.jface.text.contentassist.ICompletionProposalExtension7;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.link.ILinkedModeListener;
import org.eclipse.jface.text.link.LinkedModeModel;
@@ -57,6 +63,8 @@
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.viewers.StyledString;
+import org.eclipse.jface.viewers.StyledString.Styler;
+import org.eclipse.osgi.util.TextProcessor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
@@ -66,6 +74,7 @@
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
@@ -77,7 +86,8 @@
public abstract class AbstractScriptCompletionProposal implements
IScriptCompletionProposal, ICompletionProposalExtension,
ICompletionProposalExtension2, ICompletionProposalExtension3,
- ICompletionProposalExtension5, ICompletionProposalExtension6 {
+ ICompletionProposalExtension5, ICompletionProposalExtension6,
+ ICompletionProposalExtension7 {
/**
* A class to simplify tracking a reference position in a document.
@@ -135,7 +145,7 @@
}
}
- protected static final class ExitPolicy implements IExitPolicy {
+ protected static class ExitPolicy implements IExitPolicy {
final char fExitCharacter;
private final IDocument fDocument;
@@ -192,6 +202,7 @@
private String fSortString;
private int fRelevance;
private boolean fIsInDoc;
+ private int fPatternMatchRule = -1;
private StyleRange fRememberedStyleRange;
private boolean fToggleEating;
@@ -472,6 +483,84 @@
}
@Override
+ public StyledString getStyledDisplayString(IDocument document, int offset,
+ BoldStylerProvider boldStylerProvider) {
+ StyledString styledDisplayString = new StyledString();
+ styledDisplayString.append(getStyledDisplayString());
+
+ String pattern = getPatternToEmphasizeMatch(document, offset);
+ if (pattern != null && pattern.length() > 0) {
+ String displayString = styledDisplayString.getString();
+ boolean isJavadocTag = isInDoc() && displayString.charAt(0) == '@'
+ && pattern.charAt(0) == '@';
+ if (isJavadocTag) {
+ displayString = displayString.substring(1);
+ pattern = pattern.substring(1);
+ }
+ int patternMatchRule = getPatternMatchRule(pattern, displayString);
+ int[] matchingRegions = SearchPattern.getMatchingRegions(pattern,
+ displayString, patternMatchRule);
+ if (isJavadocTag && matchingRegions != null) {
+ Strings.markMatchingRegions(styledDisplayString, 0,
+ new int[] { 0, 1 }, boldStylerProvider.getBoldStyler());
+ for (int i = 0; i < matchingRegions.length; i += 2) {
+ matchingRegions[i]++;
+ }
+ }
+ Strings.markMatchingRegions(styledDisplayString, 0, matchingRegions,
+ boldStylerProvider.getBoldStyler());
+ }
+ return styledDisplayString;
+ }
+
+ protected int getPatternMatchRule(String pattern, String string) {
+ String start;
+ try {
+ start = string.substring(0, pattern.length());
+ } catch (StringIndexOutOfBoundsException e) {
+ String message = "Error retrieving proposal text.\nDisplay string:\n" //$NON-NLS-1$
+ + string + "\nPattern:\n" + pattern; //$NON-NLS-1$
+ DLTKUIPlugin.log(new Status(IStatus.ERROR,
+ DLTKUIPlugin.getPluginId(), IStatus.OK, message, e));
+ return -1;
+ }
+ if (start.equalsIgnoreCase(pattern)) {
+ return SearchPattern.R_PREFIX_MATCH;
+ } else if (isCamelCaseMatching() && CharOperation
+ .camelCaseMatch(pattern.toCharArray(), string.toCharArray())) {
+ return SearchPattern.R_CAMELCASE_MATCH;
+ } else if (isSubstringMatching() && CharOperation
+ .substringMatch(pattern.toCharArray(), string.toCharArray())) {
+ return SearchPattern.R_SUBSTRING_MATCH;
+ } else {
+ return -1;
+ }
+ }
+
+ protected String getPatternToEmphasizeMatch(IDocument document,
+ int offset) {
+ int start = getPrefixCompletionStart(document, offset);
+ int patternLength = offset - start;
+ String pattern = null;
+ try {
+ pattern = document.get(start, patternLength);
+ } catch (BadLocationException e) {
+ // return null
+ }
+ return pattern;
+ }
+
+ /**
+ * Returns true if substring matching is enabled.
+ *
+ * @return <code>true</code> if substring matching is enabled
+ */
+ protected boolean isSubstringMatching() {
+ String value = DLTKCore.getOption(DLTKCore.CODEASSIST_SUBSTRING_MATCH);
+ return DLTKCore.ENABLED.equals(value);
+ }
+
+ @Override
public String getAdditionalProposalInfo() {
final Object info = getAdditionalProposalInfo(new NullProgressMonitor());
return info != null ? info.toString() : null;
@@ -673,7 +762,7 @@
* See http://dev.eclipse.org/bugs/show_bug.cgi?id=17667 why we do not
* use the replacement string. String word= fReplacementString;
*/
- return isPrefix(prefix, getDisplayString());
+ return isPrefix(prefix, TextProcessor.deprocess(getDisplayString()));
}
/**
@@ -683,6 +772,9 @@
*/
@Override
public int getRelevance() {
+ if (fPatternMatchRule == SearchPattern.R_SUBSTRING_MATCH) {
+ return fRelevance - 400;
+ }
return fRelevance;
}
@@ -725,11 +817,8 @@
if (prefix == null || string == null
|| prefix.length() > string.length())
return false;
- String start = string.substring(0, prefix.length());
- return start.equalsIgnoreCase(prefix)
- || isCamelCaseMatching()
- && CharOperation.camelCaseMatch(prefix.toCharArray(),
- string.toCharArray());
+ fPatternMatchRule = getPatternMatchRule(prefix, string);
+ return fPatternMatchRule != -1;
}
/**
@@ -1018,4 +1107,26 @@
}
return null;
}
+
+ /**
+ * Returns a deep copy of the given styles string.
+ */
+ public static StyledString copyStyledString(
+ final StyledString displayString) {
+ final StyledString copy = new StyledString(displayString.getString());
+ for (final StyleRange range : displayString.getStyleRanges()) {
+ copy.setStyle(range.start, range.length, new Styler() {
+
+ @Override
+ public void applyStyles(final TextStyle textStyle) {
+ textStyle.background = range.background;
+ textStyle.borderColor = range.borderColor;
+ textStyle.borderStyle = range.borderStyle;
+ textStyle.font = range.font;
+ textStyle.foreground = range.foreground;
+ }
+ });
+ }
+ return copy;
+ }
}
diff --git a/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/text/completion/LazyScriptCompletionProposal.java b/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/text/completion/LazyScriptCompletionProposal.java
index 21ac229..f626c34 100644
--- a/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/text/completion/LazyScriptCompletionProposal.java
+++ b/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/text/completion/LazyScriptCompletionProposal.java
@@ -201,7 +201,7 @@
}
@Override
- public final StyledString getStyledDisplayString() {
+ public StyledString getStyledDisplayString() {
if (!fDisplayStyledStringComputed) {
setStyledDisplayString(computeStyledDisplayString());
}
@@ -266,7 +266,7 @@
}
@Override
- public final int getPrefixCompletionStart(IDocument document,
+ public int getPrefixCompletionStart(IDocument document,
int completionOffset) {
return getReplacementOffset();
}