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>
+	 *            &nbsp;&nbsp;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();
 	}