Bug 573712: Reduce work done at Eclipse startup time on main thread

In Bug 466650, the git bash detector was removed from startup to reduce
overhead on startup. Later during a rewrite it was added back in under
a different code path for Bug 473107. In the context of Bug 573712
which is going to add more detectors that may do much more work on
a full load, reduce the amount of work needed to do the presence check.

Change-Id: If1ae3f12ec51b1edc2d419f0efd89fed81a7b56e
diff --git a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/internal/ExternalExecutablesState.java b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/internal/ExternalExecutablesState.java
index 4b69253..3cec5ce 100644
--- a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/internal/ExternalExecutablesState.java
+++ b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/internal/ExternalExecutablesState.java
@@ -12,7 +12,6 @@
 package org.eclipse.tm.terminal.view.ui.internal;
 
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 import org.eclipse.tm.terminal.view.ui.local.showin.ExternalExecutablesManager;
@@ -27,8 +26,7 @@
 	private boolean enabled;
 
 	public ExternalExecutablesState() {
-		List<Map<String, String>> externals = ExternalExecutablesManager.load();
-		this.enabled = (externals != null && !externals.isEmpty());
+		this.enabled = ExternalExecutablesManager.hasEntries();
 	}
 
 	@Override
diff --git a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/ExternalExecutablesManager.java b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/ExternalExecutablesManager.java
index 466593d..54afd50 100644
--- a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/ExternalExecutablesManager.java
+++ b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/ExternalExecutablesManager.java
@@ -41,6 +41,37 @@
 	// XXX: This may make a useful extension point?
 	private static List<IDetectExternalExecutable> detectors = List.of(new DetectGitBash());
 
+	public static boolean hasEntries() {
+		IPath stateLocation = UIPlugin.getDefault().getStateLocation();
+		if (stateLocation != null) {
+			File f = stateLocation.append(".executables/data.properties").toFile(); //$NON-NLS-1$
+			if (f.canRead()) {
+
+				try (FileReader r = new FileReader(f)) {
+					Properties data = new Properties();
+					data.load(r);
+					if (!data.isEmpty()) {
+						return true;
+					}
+				} catch (IOException e) {
+					if (Platform.inDebugMode()) {
+						e.printStackTrace();
+					}
+				}
+			}
+		}
+
+		// There are not any saved entries - run the detectors and if any of them
+		// have entries, then we can stop.
+		for (IDetectExternalExecutable iDetectExternalExecutable : detectors) {
+			if (iDetectExternalExecutable.hasEntries()) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
 	/**
 	 * Loads the list of all saved external executables.
 	 *
@@ -111,7 +142,7 @@
 		}
 
 		var readOnly = Collections.unmodifiableList(externalExecutables);
-		var detected = detectors.stream().flatMap(detector -> detector.run(readOnly).stream())
+		var detected = detectors.stream().flatMap(detector -> detector.getEntries(readOnly).stream())
 				.collect(Collectors.toList());
 		if (!detected.isEmpty()) {
 			externalExecutables.addAll(detected);
diff --git a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/IDetectExternalExecutable.java b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/IDetectExternalExecutable.java
index b49d5ff..4fd8083 100644
--- a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/IDetectExternalExecutable.java
+++ b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/IDetectExternalExecutable.java
@@ -15,9 +15,14 @@
 
 import org.eclipse.tm.terminal.view.ui.interfaces.IExternalExecutablesProperties;
 
-@FunctionalInterface
 public interface IDetectExternalExecutable {
 	/**
+	 * Detect if {@link #getEntries(List)} will return a non-empty list of entries, assuming externalEntries is an empty
+	 * list. This method is used during critical UI times (such as startup) and should be very fast.
+	 */
+	boolean hasEntries();
+
+	/**
 	 * Detect any additional external executables that can be added to the Show In list.
 	 *
 	 * This method is sometimes called in the UI thread when displaying context menus, so should
@@ -31,5 +36,6 @@
 	 * that match {@link IExternalExecutablesProperties}. Must not return <code>null</code>, return
 	 * an empty list instead.
 	 */
-	List<Map<String, String>> run(List<Map<String, String>> externalExecutables);
+	List<Map<String, String>> getEntries(List<Map<String, String>> externalExecutables);
+
 }
diff --git a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/detectors/DetectGitBash.java b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/detectors/DetectGitBash.java
index 170f3f6..4d056b4 100644
--- a/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/detectors/DetectGitBash.java
+++ b/terminal/plugins/org.eclipse.tm.terminal.view.ui/src/org/eclipse/tm/terminal/view/ui/local/showin/detectors/DetectGitBash.java
@@ -29,7 +29,20 @@
 	private static boolean gitBashSearchDone = false;
 
 	@Override
-	public List<Map<String, String>> run(List<Map<String, String>> externalExecutables) {
+	public boolean hasEntries() {
+		if (Platform.OS_WIN32.equals(Platform.getOS())) {
+			// the order of operations here is different than getEntries because we just
+			// want to know if anything can match. We avoid iterating over the whole PATH
+			// if the user has git installed in the default location. This gets us the
+			// correct answer to hasEntries quickly without having to iterate over the whole
+			// PATH (especially if PATH is long!).
+			return getGitInstallDirectory().or(this::getGitInstallDirectoryFromPATH).isPresent();
+		}
+		return false;
+	}
+
+	@Override
+	public List<Map<String, String>> getEntries(List<Map<String, String>> externalExecutables) {
 		// Lookup git bash (Windows Hosts only)
 		if (!gitBashSearchDone && Platform.OS_WIN32.equals(Platform.getOS())) {
 			// Do not search again for git bash while the session is running
@@ -42,29 +55,9 @@
 				return Collections.emptyList();
 			}
 
-			// If not found in the existing entries, check the path
-			Optional<File> result = ExternalExecutablesUtils.visitPATH(entry -> {
-				File f = new File(entry, "git.exe"); //$NON-NLS-1$
-				if (f.canRead()) {
-					File check = f.getParentFile().getParentFile();
-					if (new File(check, "bin/sh.exe").canExecute()) { //$NON-NLS-1$
-						return Optional.of(check);
-					}
-				}
-				return Optional.empty();
-			});
+			// If not found in the existing entries, check the path, then
 			// if it is not found in the PATH, check the default install locations
-			result = result.or(() -> {
-				File f = new File("C:/Program Files (x86)/Git/bin/sh.exe"); //$NON-NLS-1$
-				if (!f.exists()) {
-					f = new File("C:/Program Files/Git/bin/sh.exe"); //$NON-NLS-1$
-				}
-				if (f.exists() && f.canExecute()) {
-					return Optional.of(f.getParentFile().getParentFile());
-				}
-				return Optional.empty();
-			});
-
+			Optional<File> result = getGitInstallDirectoryFromPATH().or(this::getGitInstallDirectory);
 			Optional<String> gitPath = result.map(f -> new File(f, "bin/sh.exe").getAbsolutePath()); //$NON-NLS-1$
 			Optional<String> iconPath = result.flatMap(f -> getGitIconPath(f));
 
@@ -81,7 +74,33 @@
 
 		}
 		return Collections.emptyList();
+	}
 
+	private Optional<File> getGitInstallDirectoryFromPATH() {
+		return ExternalExecutablesUtils.visitPATH(entry -> {
+			File f = new File(entry, "git.exe"); //$NON-NLS-1$
+			if (f.canRead()) {
+				File check = f.getParentFile().getParentFile();
+				if (new File(check, "bin/sh.exe").canExecute()) { //$NON-NLS-1$
+					return Optional.of(check);
+				}
+			}
+			return Optional.empty();
+		});
+	}
+
+	private Optional<File> getGitInstallDirectory() {
+		// If 32-bit and 64-bit are both present, prefer the 64-bit one
+		// as it is probably newer and more likely to be present
+		// for the fast check required by hasEntries
+		File f = new File("C:/Program Files/Git/bin/sh.exe"); //$NON-NLS-1$
+		if (!f.exists()) {
+			f = new File("C:/Program Files (x86)/Git/bin/sh.exe"); //$NON-NLS-1$
+		}
+		if (f.exists() && f.canExecute()) {
+			return Optional.of(f.getParentFile().getParentFile());
+		}
+		return Optional.empty();
 	}
 
 	private static Optional<String> getGitIconPath(File parent) {