Bug 574120 - URLHyperlinkDetector: detect leading stop characters

If a scheme is followed immediately by stop characters after "://",
URLHyperlinkDetector wrongly computed hyperlink regions. Handle
this case.

Change-Id: I8e1f9b8103627008eb2fd2a46d5612f09c5b801b
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
Reviewed-on: https://git.eclipse.org/r/c/platform/eclipse.platform.text/+/181767
Tested-by: Platform Bot <platform-bot@eclipse.org>
Reviewed-by: Mickael Istria <mistria@redhat.com>
diff --git a/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/TextViewerTest.java b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/TextViewerTest.java
index 2bf06c6..a56d083 100644
--- a/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/TextViewerTest.java
+++ b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/TextViewerTest.java
@@ -20,7 +20,9 @@
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeNotNull;
 
+import java.util.Arrays;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
 
 import org.junit.Assume;
 import org.junit.Rule;
@@ -42,15 +44,20 @@
 
 import org.eclipse.jface.util.Util;
 
+import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.BlockTextSelection;
 import org.eclipse.jface.text.Document;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IDocumentAdapter;
 import org.eclipse.jface.text.IDocumentExtension3;
+import org.eclipse.jface.text.IRegion;
 import org.eclipse.jface.text.ITextOperationTarget;
 import org.eclipse.jface.text.ITextSelection;
 import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.Region;
 import org.eclipse.jface.text.TextViewer;
+import org.eclipse.jface.text.hyperlink.IHyperlink;
+import org.eclipse.jface.text.hyperlink.URLHyperlinkDetector;
 import org.eclipse.jface.text.source.SourceViewer;
 import org.eclipse.jface.text.tests.util.DisplayHelper;
 
@@ -367,4 +374,49 @@
 			shell.dispose();
 		}
 	}
+
+	private String toString(Document document, IHyperlink[] links) {
+		if (links == null) {
+			return "[]";
+		}
+		return Arrays.stream(links).map(l -> {
+			IRegion region= l.getHyperlinkRegion();
+			try {
+				return document.get(region.getOffset(), region.getLength());
+			} catch (BadLocationException e) {
+				return "Invalid region <" + region + '>';
+			}
+		}).collect(Collectors.joining(",", "[", "]"));
+	}
+
+	private void checkHyperlink(TextViewer textViewer, int pos, String text, String expected) {
+		Document document= new Document(text);
+		textViewer.setDocumentPartitioning(IDocumentExtension3.DEFAULT_PARTITIONING);
+		textViewer.setDocument(document);
+		IRegion region= new Region(pos, 0);
+		URLHyperlinkDetector detector= new URLHyperlinkDetector();
+		IHyperlink[] hyperlinks= detector.detectHyperlinks(textViewer, region, false);
+		String found= toString(document, hyperlinks);
+		assertEquals(expected, found);
+	}
+
+	@Test
+	public void testURLHyperlinkDetector() {
+		Shell shell = new Shell();
+
+		try {
+			TextViewer textViewer= new TextViewer(shell, SWT.NONE);
+			checkHyperlink(textViewer, 3, "https://foo ", "[https://foo]");
+			checkHyperlink(textViewer, 0, "", "[]");
+			checkHyperlink(textViewer, 3, "https", "[]");
+			checkHyperlink(textViewer, 3, "https://", "[]");
+			checkHyperlink(textViewer, 3, "https:// ", "[]");
+			checkHyperlink(textViewer, 3, "https:// foo", "[]");
+			checkHyperlink(textViewer, 3, "https://foo bar", "[https://foo]");
+			checkHyperlink(textViewer, 15, "https:// foo https://bar bar", "[https://bar]");
+			checkHyperlink(textViewer, 24, "https:// foo https://bar bar", "[https://bar]");
+		} finally {
+			shell.dispose();
+		}
+	}
 }
diff --git a/org.eclipse.jface.text/src/org/eclipse/jface/text/hyperlink/URLHyperlinkDetector.java b/org.eclipse.jface.text/src/org/eclipse/jface/text/hyperlink/URLHyperlinkDetector.java
index ba558f5..5da9cfa 100644
--- a/org.eclipse.jface.text/src/org/eclipse/jface/text/hyperlink/URLHyperlinkDetector.java
+++ b/org.eclipse.jface.text/src/org/eclipse/jface/text/hyperlink/URLHyperlinkDetector.java
@@ -16,7 +16,6 @@
 
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.StringTokenizer;
 
 import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.IDocument;
@@ -32,6 +31,7 @@
  */
 public class URLHyperlinkDetector extends AbstractHyperlinkDetector {
 
+	private static final String STOP_CHARACTERS= " \t\n\r\f<>"; //$NON-NLS-1$
 
 	/**
 	 * Creates a new URL hyperlink detector.
@@ -78,6 +78,7 @@
 		char quote= 0;
 		int urlOffsetInLine= 0;
 		int urlLength= 0;
+		int lineEnd= line.length();
 
 		int urlSeparatorOffset= line.indexOf("://"); //$NON-NLS-1$
 		while (urlSeparatorOffset >= 0) {
@@ -96,15 +97,18 @@
 			urlOffsetInLine++;
 
 			// Right to "://"
-			StringTokenizer tokenizer= new StringTokenizer(line.substring(urlSeparatorOffset + 3), " \t\n\r\f<>", false); //$NON-NLS-1$
-			if (!tokenizer.hasMoreTokens())
-				return null;
+			int end= urlSeparatorOffset + 3;
+			while (end < lineEnd && STOP_CHARACTERS.indexOf(line.charAt(end)) < 0) {
+				end++;
+			}
+			if (end > urlSeparatorOffset + 3) {
+				urlLength= end - urlOffsetInLine;
+				if (offsetInLine >= urlOffsetInLine && offsetInLine <= urlOffsetInLine + urlLength) {
+					break;
+				}
+			}
 
-			urlLength= tokenizer.nextToken().length() + 3 + urlSeparatorOffset - urlOffsetInLine;
-			if (offsetInLine >= urlOffsetInLine && offsetInLine <= urlOffsetInLine + urlLength)
-				break;
-
-			urlSeparatorOffset= line.indexOf("://", urlSeparatorOffset + 1); //$NON-NLS-1$
+			urlSeparatorOffset= line.indexOf("://", urlSeparatorOffset + 3); //$NON-NLS-1$
 		}
 
 		if (urlSeparatorOffset < 0)