Bug 563485 - Add StringMatcher.usePrefixMatch()

Add a method to make the StringMatcher match on prefixes. Internally
this just switches on wildcard matching and sets fHasTrailingStar to
true. Because the pattern has already been parsed in the constructor,
this gives an easy way to specify prefix matching for non-wildcard
patterns without needing to escape potential wildcards in the pattern:

  StringMatcher m = new StringMatcher("foo*bar", false, true);
  m.usePrefixMatch();

This matcher will match any text starting with "foo*bar" (with a
literal '*'), but not, for instance, "foobazbar".

Change-Id: I525fbc1d42b16d661988655c8438894f8d6739e8
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
diff --git a/bundles/org.eclipse.equinox.common.tests/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.common.tests/META-INF/MANIFEST.MF
index c5b6a6e..54044be 100644
--- a/bundles/org.eclipse.equinox.common.tests/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.common.tests/META-INF/MANIFEST.MF
@@ -7,7 +7,7 @@
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Bundle-ActivationPolicy: lazy
 Require-Bundle: org.junit,
- org.eclipse.equinox.common;bundle-version="3.12.0",
+ org.eclipse.equinox.common;bundle-version="3.13.0",
  org.eclipse.core.tests.harness;bundle-version="3.11.400",
  org.eclipse.equinox.registry;bundle-version="3.8.200"
 Import-Package: org.eclipse.osgi.service.localization,
diff --git a/bundles/org.eclipse.equinox.common.tests/src/org/eclipse/equinox/common/tests/text/StringMatcherOtherTest.java b/bundles/org.eclipse.equinox.common.tests/src/org/eclipse/equinox/common/tests/text/StringMatcherOtherTest.java
new file mode 100644
index 0000000..d4dc5ad
--- /dev/null
+++ b/bundles/org.eclipse.equinox.common.tests/src/org/eclipse/equinox/common/tests/text/StringMatcherOtherTest.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (C) 2020, Thomas Wolf <thomas.wolf@paranor.ch> and others.
+ *
+ * All rights reserved. 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
+ *******************************************************************************/
+package org.eclipse.equinox.common.tests.text;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.core.text.StringMatcher;
+import org.junit.Test;
+
+public class StringMatcherOtherTest {
+
+	@Test
+	public void testEmptyNoWildcard() {
+		StringMatcher m = new StringMatcher("", false, true);
+		assertTrue(m.match(""));
+		assertFalse(m.match("foo"));
+		assertFalse(m.match("foo bar"));
+	}
+
+	@Test
+	public void testEmptyWildcard() {
+		StringMatcher m = new StringMatcher("", false, false);
+		assertTrue(m.match(""));
+		assertFalse(m.match("foo"));
+		assertFalse(m.match("foo bar"));
+	}
+}
diff --git a/bundles/org.eclipse.equinox.common.tests/src/org/eclipse/equinox/common/tests/text/StringMatcherPrefixTest.java b/bundles/org.eclipse.equinox.common.tests/src/org/eclipse/equinox/common/tests/text/StringMatcherPrefixTest.java
new file mode 100644
index 0000000..89d39c4
--- /dev/null
+++ b/bundles/org.eclipse.equinox.common.tests/src/org/eclipse/equinox/common/tests/text/StringMatcherPrefixTest.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * Copyright (C) 2020, Thomas Wolf <thomas.wolf@paranor.ch> and others.
+ *
+ * All rights reserved. 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
+ *******************************************************************************/
+package org.eclipse.equinox.common.tests.text;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.core.text.StringMatcher;
+import org.junit.Test;
+
+public class StringMatcherPrefixTest {
+
+	@Test
+	public void testNoPrefix() {
+		StringMatcher m = new StringMatcher("foo", false, true);
+		assertTrue(m.match("foo"));
+		assertFalse(m.match("foobar"));
+		assertFalse(m.match("foo bar"));
+		assertFalse(m.match("bar foo"));
+		assertFalse(m.match("bar foo bar"));
+	}
+
+	@Test
+	public void testEmptyNoWildcard() {
+		StringMatcher m = new StringMatcher("", false, true);
+		m.usePrefixMatch();
+		assertTrue(m.match(""));
+		assertFalse(m.match("foo"));
+		assertFalse(m.match("foo bar"));
+	}
+
+	@Test
+	public void testEmptyWildcard() {
+		StringMatcher m = new StringMatcher("", false, false);
+		m.usePrefixMatch();
+		assertTrue(m.match(""));
+		assertTrue(m.match("foo"));
+		assertTrue(m.match("foo bar"));
+	}
+
+	@Test
+	public void testPrefixNoWildcards() {
+		StringMatcher m = new StringMatcher("foo", false, true);
+		m.usePrefixMatch();
+		assertTrue(m.match("foo"));
+		assertTrue(m.match("foobar"));
+		assertTrue(m.match("foo bar"));
+		assertFalse(m.match("bar foo"));
+		assertFalse(m.match("bar foo bar"));
+	}
+
+	@Test
+	public void testPrefixWildcards() {
+		StringMatcher m = new StringMatcher("f?o", false, false);
+		m.usePrefixMatch();
+		assertTrue(m.match("foo"));
+		assertTrue(m.match("foobar"));
+		assertTrue(m.match("foo bar"));
+		assertFalse(m.match("bar foo"));
+		assertFalse(m.match("bar foo bar"));
+	}
+
+	@Test
+	public void testPrefixWildcardsOffSingle() {
+		StringMatcher m = new StringMatcher("f?o", false, true);
+		m.usePrefixMatch();
+		assertFalse(m.match("foo"));
+		assertTrue(m.match("f?o"));
+		assertFalse(m.match("foobar"));
+		assertTrue(m.match("f?obar"));
+		assertTrue(m.match("f?o bar"));
+		assertFalse(m.match("bar f?o"));
+		assertFalse(m.match("bar f?o bar"));
+	}
+
+	@Test
+	public void testPrefixWildcardsOffMulti() {
+		StringMatcher m = new StringMatcher("foo*bar", false, true);
+		m.usePrefixMatch();
+		assertFalse(m.match("foobar"));
+		assertFalse(m.match("foobazbar"));
+		assertTrue(m.match("foo*bar"));
+		assertFalse(m.match("foobarbaz"));
+		assertTrue(m.match("foo*barbaz"));
+		assertFalse(m.match("bar foo*bar"));
+		assertFalse(m.match("bar foo*barbaz"));
+	}
+}
diff --git a/bundles/org.eclipse.equinox.common.tests/src/org/eclipse/equinox/common/tests/text/StringMatcherTests.java b/bundles/org.eclipse.equinox.common.tests/src/org/eclipse/equinox/common/tests/text/StringMatcherTests.java
index dc40743..7e9f546 100644
--- a/bundles/org.eclipse.equinox.common.tests/src/org/eclipse/equinox/common/tests/text/StringMatcherTests.java
+++ b/bundles/org.eclipse.equinox.common.tests/src/org/eclipse/equinox/common/tests/text/StringMatcherTests.java
@@ -1,3 +1,13 @@
+/*******************************************************************************
+ * Copyright (C) 2020, Thomas Wolf <thomas.wolf@paranor.ch> and others.
+ *
+ * All rights reserved. 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
+ *******************************************************************************/
 package org.eclipse.equinox.common.tests.text;
 
 import org.junit.runner.RunWith;
@@ -6,9 +16,11 @@
 
 @RunWith(Suite.class)
 @SuiteClasses({
-	StringMatcherFindTest.class, 
-	StringMatcherPlainTest.class, 
-	StringMatcherWildcardTest.class
+	StringMatcherFindTest.class,
+	StringMatcherPlainTest.class,
+	StringMatcherWildcardTest.class,
+	StringMatcherPrefixTest.class,
+	StringMatcherOtherTest.class
 })
 public class StringMatcherTests {
 	// empty
diff --git a/bundles/org.eclipse.equinox.common/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.common/META-INF/MANIFEST.MF
index 0f35a08..fb17d96 100644
--- a/bundles/org.eclipse.equinox.common/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.common/META-INF/MANIFEST.MF
@@ -15,7 +15,7 @@
    org.eclipse.core.filesystem,
    org.eclipse.equinox.security",
  org.eclipse.core.runtime;common=split;version="3.6.0";mandatory:=common,
- org.eclipse.core.text;version="3.12.0",
+ org.eclipse.core.text;version="3.13.0",
  org.eclipse.equinox.events;version="1.0.0"
 Bundle-Vendor: %providerName
 Bundle-Activator: org.eclipse.core.internal.runtime.Activator
diff --git a/bundles/org.eclipse.equinox.common/src/org/eclipse/core/text/StringMatcher.java b/bundles/org.eclipse.equinox.common/src/org/eclipse/core/text/StringMatcher.java
index 5183ff9..b4a3371 100644
--- a/bundles/org.eclipse.equinox.common/src/org/eclipse/core/text/StringMatcher.java
+++ b/bundles/org.eclipse.equinox.common/src/org/eclipse/core/text/StringMatcher.java
@@ -27,17 +27,17 @@
 
 	private final int fLength; // pattern length
 
-	private final boolean fIgnoreWildCards;
-
 	private final boolean fIgnoreCase;
 
+	private boolean fIgnoreWildCards;
+
 	private boolean fHasLeadingStar;
 
 	private boolean fHasTrailingStar;
 
 	private String fSegments[]; // the given pattern is split into * separated segments
 
-	/* Boundary value beyond which we don't need to search in the text. */
+	/* Minimum length required for a match: shorter texts cannot possibly match. */
 	private int fBound = 0;
 
 	private static final char fSingleWildCard = '\u0000';
@@ -116,21 +116,28 @@
 	 * StringMatcher constructor takes in a String object that is a simple pattern.
 	 * The pattern may contain '*' for 0 and many characters and '?' for exactly one
 	 * character.
-	 *
+	 * <p>
 	 * Literal '*' and '?' characters must be escaped in the pattern e.g., "\*"
 	 * means literal "*", etc.
-	 *
+	 * </p>
+	 * <p>
 	 * The escape character '\' is an escape only if followed by '*', '?', or '\'.
 	 * All other occurrences are taken literally.
-	 *
+	 * </p>
+	 * <p>
 	 * If invoking the StringMatcher with string literals in Java, don't forget
 	 * escape characters are represented by "\\".
+	 * </p>
+	 * <p
+	 * An empty pattern matches only an empty text, unless {@link #usePrefixMatch()}
+	 * is used, in which case it always matches.
+	 * </p>
 	 *
 	 * @param pattern         the pattern to match text against, must not be {@code null}
 	 * @param ignoreCase      if true, case is ignored
 	 * @param ignoreWildCards if true, wild cards and their escape sequences are
 	 *                        ignored (everything is taken literally).
-	 * @throws IllegalArgumentException if {@code pattern == null} 
+	 * @throws IllegalArgumentException if {@code pattern == null}
 	 */
 	public StringMatcher(String pattern, boolean ignoreCase, boolean ignoreWildCards) {
 		if (pattern == null) {
@@ -149,6 +156,23 @@
 	}
 
 	/**
+	 * Configures this {@link StringMatcher} to also match on prefix-only matches.
+	 * <p>
+	 * If the matcher was created with {@code ignoreWildCards == true}, any wildcard
+	 * characters in the pattern will still be matched literally.
+	 * </p>
+	 * <p>
+	 * If the pattern is empty, it will match any text.
+	 * </p>
+	 *
+	 * @since 3.13
+	 */
+	public void usePrefixMatch() {
+		fIgnoreWildCards = false;
+		fHasTrailingStar = true;
+	}
+
+	/**
 	 * Finds the first occurrence of the pattern between {@code start} (inclusive)
 	 * and {@code end} (exclusive).
 	 * <p>
@@ -485,4 +509,23 @@
 		}
 		return true;
 	}
+
+	@Override
+	public String toString() {
+		String flags = ""; //$NON-NLS-1$
+		if (fIgnoreCase) {
+			flags += 'i';
+		}
+		if (fHasTrailingStar) {
+			flags += 't';
+		}
+		if (!fIgnoreWildCards) {
+			flags += '*';
+		}
+		String result = '[' + fPattern;
+		if (!flags.isEmpty()) {
+			result += '/' + flags;
+		}
+		return result + ']';
+	}
 }