Bug 458704 - Fix failing FileSearchTest

Update the TextSearchRequestor's API to support parallelization
and fix existing subclasses and tests.

Also add an optimization to process all files in a single job if
the maximum number of threads for the job group is 1, which can
occur on single processor machines or if the TextSearchRequestor
passed to the TextSearchEngine.search() does not support
parallelism. This removes some unnecessary job scheduling.

Change-Id: I5baaf1dc3910a527338dc12e7fa4575269470833
Signed-off-by: Terry Parker <tparker@google.com>
diff --git a/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/FileSearchTests.java b/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/FileSearchTests.java
index 8d59a01..cdfcf20 100644
--- a/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/FileSearchTests.java
+++ b/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/FileSearchTests.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2013 IBM Corporation and others.
+ * Copyright (c) 2000, 2015 IBM Corporation 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
@@ -8,6 +8,7 @@
  * Contributors:
  *     IBM Corporation - initial API and implementation
  *     Christian Walther (Indel AG) - Bug 399094, 402009: Add whole word option to file search
+ *     Terry Parker <tparker@google.com> (Google Inc.) - Bug 441016 - Speed up text search by parallelizing it using JobGroups
  *******************************************************************************/
 package org.eclipse.search.tests.filesearch;
 
@@ -51,31 +52,57 @@
 			this.length= length;
 		}
 	}
-	
-	
+
 	private static class TestResultCollector extends TextSearchRequestor {
-		
-		private List fResult;
+
+		protected List fResult;
 
 		public TestResultCollector() {
+			reset();
+		}
+
+		public TestResult[] getResults() {
+			return (TestResult[]) fResult.toArray(new TestResult[fResult.size()]);
+		}
+
+		public int getNumberOfResults() {
+			return fResult.size();
+		}
+
+		public void reset() {
 			fResult= new ArrayList();
 		}
-		
+
+	}
+
+	private static class SerialTestResultCollector extends TestResultCollector {
+
+		public boolean canRunInParallel() {
+			return false;
+		}
+
 		public boolean acceptPatternMatch(TextSearchMatchAccess match) throws CoreException {
 			fResult.add(new TestResult(match.getFile(), match.getMatchOffset(), match.getMatchLength()));
 			return true;
 		}
-				
-		public TestResult[] getResults() {
-			return (TestResult[]) fResult.toArray(new TestResult[fResult.size()]);
-		}
-		
-		public int getNumberOfResults() {
-			return fResult.size();
-		}
-		
+
 	}
-	
+
+	private static class ParallelTestResultCollector extends TestResultCollector {
+
+		public boolean canRunInParallel() {
+			return true;
+		}
+
+		public boolean acceptPatternMatch(TextSearchMatchAccess match) throws CoreException {
+			synchronized(fResult) {
+				fResult.add(new TestResult(match.getFile(), match.getMatchOffset(), match.getMatchLength()));
+			}
+			return true;
+		}
+
+	}
+
 	private IProject fProject;
 	
 	public FileSearchTests(String name) {
@@ -101,9 +128,16 @@
 	protected void tearDown() throws Exception {
 		ResourceHelper.deleteProject("my-project"); //$NON-NLS-1$
 	}
-	
-	
-	public void testSimpleFiles() throws Exception {
+
+	public void testSimpleFilesSerial() throws Exception {
+		testSimpleFiles(new SerialTestResultCollector());
+	}
+
+	public void testSimpleFilesParallel() throws Exception {
+		testSimpleFiles(new ParallelTestResultCollector());
+	}
+
+	private void testSimpleFiles(TestResultCollector collector) throws Exception {
 		StringBuffer buf= new StringBuffer();
 		buf.append("File1\n");
 		buf.append("hello\n");
@@ -113,20 +147,27 @@
 		IFile file1= ResourceHelper.createFile(folder, "file1", buf.toString());
 		IFile file2= ResourceHelper.createFile(folder, "file2", buf.toString());
 
-		TestResultCollector collector= new TestResultCollector();
 		Pattern searchPattern= PatternConstructor.createPattern("hello", false, true);
 
 		FileTextSearchScope scope= FileTextSearchScope.newSearchScope(new IResource[] {fProject}, (String[]) null, false);
 		TextSearchEngine.create().search(scope, collector, searchPattern, null);
-		
+
 		TestResult[] results= collector.getResults();
 		assertEquals("Number of total results", 4, results.length);
-		
+
 		assertMatches(results, 2, file1, buf.toString(), "hello");
 		assertMatches(results, 2, file2, buf.toString(), "hello");
 	}
-	
-	public void testWildCards1() throws Exception {
+
+	public void testWildCards1Serial() throws Exception {
+		testWildCards1(new SerialTestResultCollector());
+	}
+
+	public void testWildCards1Parallel() throws Exception {
+		testWildCards1(new ParallelTestResultCollector());
+	}
+
+	private void testWildCards1(TestResultCollector collector) throws Exception {
 		StringBuffer buf= new StringBuffer();
 		buf.append("File1\n");
 		buf.append("no more\n");
@@ -137,7 +178,6 @@
 		ResourceHelper.createFile(folder, "file1", buf.toString());
 		ResourceHelper.createFile(folder, "file2", buf.toString());
 
-		TestResultCollector collector= new TestResultCollector();
 		Pattern searchPattern= PatternConstructor.createPattern("mor*", false, false);
 		
 		FileTextSearchScope scope= FileTextSearchScope.newSearchScope(new IResource[] {fProject}, (String[]) null, false);
@@ -146,8 +186,16 @@
 		TestResult[] results= collector.getResults();
 		assertEquals("Number of total results", 6, results.length);
 	}
-	
-	public void testWildCards2() throws Exception {
+
+	public void testWildCards2Serial() throws Exception {
+		testWildCards2(new SerialTestResultCollector());
+	}
+
+	public void testWildCards2Parallel() throws Exception {
+		testWildCards2(new ParallelTestResultCollector());
+	}
+
+	private void testWildCards2(TestResultCollector collector) throws Exception {
 		StringBuffer buf= new StringBuffer();
 		buf.append("File1\n");
 		buf.append("no more\n");
@@ -158,7 +206,6 @@
 		ResourceHelper.createFile(folder, "file1", buf.toString());
 		ResourceHelper.createFile(folder, "file2", buf.toString());
 
-		TestResultCollector collector= new TestResultCollector();
 		Pattern searchPattern= PatternConstructor.createPattern("mo?e", false, false);
 		
 		FileTextSearchScope scope= FileTextSearchScope.newSearchScope(new IResource[] {fProject}, (String[]) null, false);
@@ -167,8 +214,16 @@
 		TestResult[] results= collector.getResults();
 		assertEquals("Number of total results", 4, results.length);
 	}
-	
-	public void testWildCards3() throws Exception {
+
+	public void testWildCards3Serial() throws Exception {
+		testWildCards3(new SerialTestResultCollector());
+	}
+
+	public void testWildCards3Parallel() throws Exception {
+		testWildCards3(new ParallelTestResultCollector());
+	}
+
+	private void testWildCards3(TestResultCollector collector) throws Exception {
 		
 		IProject project= JUnitSourceSetup.getStandardProject();
 		IFile openFile1= (IFile) project.findMember("junit/framework/TestCase.java");
@@ -185,7 +240,6 @@
 			
 			long start= System.currentTimeMillis();
 
-			TestResultCollector collector= new TestResultCollector();
 			Pattern searchPattern= PatternConstructor.createPattern("\\w*\\(\\)", false, true);
 
 			// search in Junit sources
@@ -200,11 +254,18 @@
 		} finally {
 			activePage.closeAllEditors(false);
 		}
-		
 
 	}
-	
-	public void testWholeWord() throws Exception {
+
+	public void testWholeWordSerial() throws Exception {
+		testWholeWord(new SerialTestResultCollector());
+	}
+
+	public void testWholeWordParallel() throws Exception {
+		testWholeWord(new ParallelTestResultCollector());
+	}
+
+	private void testWholeWord(TestResultCollector collector) throws Exception {
 		StringBuffer buf= new StringBuffer();
 		// nothing after
 		buf.append("hell\n"); // nothing before
@@ -230,28 +291,35 @@
 		{
 			// wildcards, whole word = false: match all lines
 			Pattern searchPattern= PatternConstructor.createPattern("h?ll", false, true, false, false);
-			TestResultCollector collector= new TestResultCollector();
+			collector.reset();
 			engine.search(scope, collector, searchPattern, null);
 			assertEquals("Number of partial-word results", 22, collector.getNumberOfResults());
 		}
 		{
 			// wildcards, whole word = true: match only nothing and non-word chars before and after
 			Pattern searchPattern= PatternConstructor.createPattern("h?ll", false, true, false, true);
-			TestResultCollector collector= new TestResultCollector();
+			collector.reset();
 			engine.search(scope, collector, searchPattern, null);
 			assertEquals("Number of whole-word results", 10, collector.getNumberOfResults());
 		}
 		{
 			// regexp, whole word = false: match all lines
 			Pattern searchPattern= PatternConstructor.createPattern("h[eio]ll", true, true, false, false);
-			TestResultCollector collector= new TestResultCollector();
+			collector.reset();
 			engine.search(scope, collector, searchPattern, null);
 			assertEquals("Number of partial-word results", 22, collector.getNumberOfResults());
 		}
 	}
-	
 
-	public void testFileOpenInEditor() throws Exception {
+	public void testFileOpenInEditorSerial() throws Exception {
+		testFileOpenInEditor(new SerialTestResultCollector());
+	}
+
+	public void testFileOpenInEditorParallel() throws Exception {
+		testFileOpenInEditor(new ParallelTestResultCollector());
+	}
+
+	private void testFileOpenInEditor(TestResultCollector collector) throws Exception {
 		StringBuffer buf= new StringBuffer();
 		buf.append("File1\n");
 		buf.append("hello\n");
@@ -264,7 +332,6 @@
 		try {
 			IDE.openEditor(SearchPlugin.getActivePage(), file2, true);
 			
-			TestResultCollector collector= new TestResultCollector();
 			Pattern searchPattern= PatternConstructor.createPattern("hello", false, true);
 
 			FileTextSearchScope scope= FileTextSearchScope.newSearchScope(new IResource[] {fProject}, (String[]) null, false);
@@ -279,8 +346,16 @@
 			SearchPlugin.getActivePage().closeAllEditors(false);
 		}
 	}
-	
-	public void testDerivedFiles() throws Exception {
+
+	public void testDerivedFilesSerial() throws Exception {
+		testDerivedFiles(new SerialTestResultCollector());
+	}
+
+	public void testDerivedFilesParallel() throws Exception {
+		testDerivedFiles(new ParallelTestResultCollector());
+	}
+
+	private void testDerivedFiles(TestResultCollector collector) throws Exception {
 		StringBuffer buf= new StringBuffer();
 		buf.append("hello\n");
 		IFolder folder1= ResourceHelper.createFolder(fProject.getFolder("folder1"));
@@ -316,50 +391,58 @@
 		TextSearchEngine engine= TextSearchEngine.create();
 		{
 			// visit all
-			TestResultCollector collector= new TestResultCollector();
 			TextSearchScope scope= TextSearchScope.newSearchScope(new IResource[] { fProject }, fileNamePattern, true);
+			collector.reset();
 			engine.search(scope, collector, searchPattern, null);
 			assertEquals(5, collector.getNumberOfResults());
 		}
 		{
 			// visit non-derived
-			TestResultCollector collector= new TestResultCollector();
 			TextSearchScope scope= TextSearchScope.newSearchScope(new IResource[] { fProject }, fileNamePattern, false);
+			collector.reset();
 			engine.search(scope, collector, searchPattern, null);
 			assertEquals(2, collector.getNumberOfResults());
 		}
 		{
 			// visit all in folder2
-			TestResultCollector collector= new TestResultCollector();
 			TextSearchScope scope= TextSearchScope.newSearchScope(new IResource[] { folder2 }, fileNamePattern, true);
+			collector.reset();
 			engine.search(scope, collector, searchPattern, null);
 			assertEquals(2, collector.getNumberOfResults());
 		}
 		{
 			// visit non-derived in folder2
-			TestResultCollector collector= new TestResultCollector();
 			TextSearchScope scope= TextSearchScope.newSearchScope(new IResource[] { folder2 }, fileNamePattern, false);
+			collector.reset();
 			engine.search(scope, collector, searchPattern, null);
 			assertEquals(0, collector.getNumberOfResults());
 		}
 		{
 			// visit all in folder3
-			TestResultCollector collector= new TestResultCollector();
 			TextSearchScope scope= TextSearchScope.newSearchScope(new IResource[] { folder3 }, fileNamePattern, true);
+			collector.reset();
 			engine.search(scope, collector, searchPattern, null);
 			assertEquals(1, collector.getNumberOfResults());
 		}
 		{
 			// visit non-derived in folder3
-			TestResultCollector collector= new TestResultCollector();
 			TextSearchScope scope= TextSearchScope.newSearchScope(new IResource[] { folder3 }, fileNamePattern, false);
+			collector.reset();
 			engine.search(scope, collector, searchPattern, null);
 			assertEquals(0, collector.getNumberOfResults());
 		}
 	}
 
-	
-	public void testFileNamePatterns() throws Exception {
+	public void testFileNamePatternsSerial() throws Exception {
+		testFileNamePatterns(new SerialTestResultCollector());
+	}
+
+	public void testFileNamePatternsParallel() throws Exception {
+		testFileNamePatterns(new ParallelTestResultCollector());
+	}
+
+
+	private void testFileNamePatterns(TestResultCollector collector) throws Exception {
 		IFolder folder= ResourceHelper.createFolder(fProject.getFolder("folder1"));
 		ResourceHelper.createFile(folder, "file1.x", "Test");
 		ResourceHelper.createFile(folder, "file2.x", "Test");
@@ -369,44 +452,44 @@
 		Pattern searchPattern= PatternConstructor.createPattern("Test", false, false);
 		String[] fileNamePatterns= { "*" };
 				
-		TestResult[] results= performSearch(fileNamePatterns, searchPattern);
+		TestResult[] results= performSearch(collector, fileNamePatterns, searchPattern);
 		assertEquals("Number of total results", 4, results.length);
 		
 		fileNamePatterns= new String[] { "*.x" };
-		results= performSearch(fileNamePatterns, searchPattern);
+		results= performSearch(collector, fileNamePatterns, searchPattern);
 		assertEquals("Number of total results", 2, results.length);
 		
 		fileNamePatterns= new String[] { "*.x", "*.y*" };
-		results= performSearch(fileNamePatterns, searchPattern);
+		results= performSearch(collector, fileNamePatterns, searchPattern);
 		assertEquals("Number of total results", 3, results.length);
 		
 		fileNamePatterns= new String[] { "!*.x" };
-		results= performSearch(fileNamePatterns, searchPattern);
+		results= performSearch(collector, fileNamePatterns, searchPattern);
 		assertEquals("Number of total results", 2, results.length);
 		
 		fileNamePatterns= new String[] { "!*.x", "!*.y" };
-		results= performSearch(fileNamePatterns, searchPattern);
+		results= performSearch(collector, fileNamePatterns, searchPattern);
 		assertEquals("Number of total results", 1, results.length);
 		
 		fileNamePatterns= new String[] { "*", "!*.y" };
-		results= performSearch(fileNamePatterns, searchPattern);
+		results= performSearch(collector, fileNamePatterns, searchPattern);
 		assertEquals("Number of total results", 3, results.length);
 		
 		fileNamePatterns= new String[] { "*", "!*.*" };
-		results= performSearch(fileNamePatterns, searchPattern);
+		results= performSearch(collector, fileNamePatterns, searchPattern);
 		assertEquals("Number of total results", 0, results.length);
 		
 		fileNamePatterns= new String[] { "*.x", "*.y*", "!*.y" };
-		results= performSearch(fileNamePatterns, searchPattern);
+		results= performSearch(collector, fileNamePatterns, searchPattern);
 		assertEquals("Number of total results", 2, results.length);
 		
 		fileNamePatterns= new String[] { "file*", "!*.x*", "!*.y" };
-		results= performSearch(fileNamePatterns, searchPattern);
+		results= performSearch(collector, fileNamePatterns, searchPattern);
 		assertEquals("Number of total results", 1, results.length);
 	}
 	
-	private TestResult[] performSearch(String[] fileNamePatterns, Pattern searchPattern) {
-		TestResultCollector collector= new TestResultCollector();
+	private TestResult[] performSearch(TestResultCollector collector, String[] fileNamePatterns, Pattern searchPattern) {
+		collector.reset();
 		FileTextSearchScope scope= FileTextSearchScope.newSearchScope(new IResource[] {fProject}, fileNamePatterns, false);
 		TextSearchEngine.create().search(scope, collector, searchPattern, null);
 		
diff --git a/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/LineBasedFileSearch.java b/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/LineBasedFileSearch.java
index b23171c..251c7f1 100644
--- a/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/LineBasedFileSearch.java
+++ b/org.eclipse.search.tests/src/org/eclipse/search/tests/filesearch/LineBasedFileSearch.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2008 IBM Corporation and others.
+ * Copyright (c) 2000, 2015 IBM Corporation 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
@@ -7,6 +7,7 @@
  *
  * Contributors:
  *     IBM Corporation - initial API and implementation
+ *     Terry Parker <tparker@google.com> (Google Inc.) - Bug 441016 - Speed up text search by parallelizing it using JobGroups
  *******************************************************************************/
 
 package org.eclipse.search.tests.filesearch;
@@ -45,13 +46,17 @@
 		private final AbstractTextSearchResult fResult;
 		private IFile fLastFile;
 		private IDocument fLastDocument;	
-		
+		private Object fLock= new Object();
+
 		private LineBasedTextSearchResultCollector(AbstractTextSearchResult result) {
 			fResult= result;
 			fLastFile= null;
 			fLastDocument= null;
 		}
 
+		public boolean canRunInParallel() {
+			return true;
+		}
 
 		/* (non-Javadoc)
 		 * @see org.eclipse.search.core.text.FileSearchRequestor#acceptPatternMatch(org.eclipse.search.core.text.FileSearchMatchRequestor)
@@ -66,7 +71,9 @@
 
 				int startLine= doc.getLineOfOffset(matchRequestor.getMatchOffset());
 				int endLine= doc.getLineOfOffset(matchRequestor.getMatchOffset() + matchRequestor.getMatchLength());
-				fResult.addMatch(new FileMatch(file, startLine, endLine - startLine + 1, null));
+				synchronized(fLock) {
+					fResult.addMatch(new FileMatch(file, startLine, endLine - startLine + 1, null));
+				}
 			} catch (BadLocationException e) {
 				throw new CoreException(new Status(IStatus.ERROR, SearchPlugin.getID(), IStatus.ERROR, "bad location", e));
 			}
diff --git a/org.eclipse.search/META-INF/MANIFEST.MF b/org.eclipse.search/META-INF/MANIFEST.MF
index c3c4ec5..aa11e5e 100644
--- a/org.eclipse.search/META-INF/MANIFEST.MF
+++ b/org.eclipse.search/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.search; singleton:=true
-Bundle-Version: 3.9.200.qualifier
+Bundle-Version: 3.10.0.qualifier
 Bundle-Activator: org.eclipse.search.internal.ui.SearchPlugin
 Bundle-ActivationPolicy: lazy
 Bundle-Vendor: %providerName
diff --git a/org.eclipse.search/new search/org/eclipse/search/core/text/TextSearchRequestor.java b/org.eclipse.search/new search/org/eclipse/search/core/text/TextSearchRequestor.java
index 7858bfa..84d3242 100644
--- a/org.eclipse.search/new search/org/eclipse/search/core/text/TextSearchRequestor.java
+++ b/org.eclipse.search/new search/org/eclipse/search/core/text/TextSearchRequestor.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2008 IBM Corporation and others.
+ * Copyright (c) 2000, 2015 IBM Corporation 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
@@ -7,6 +7,7 @@
  *
  * Contributors:
  *     IBM Corporation - initial API and implementation
+ *     Terry Parker <tparker@google.com> (Google Inc.) - Bug 441016 - Speed up text search by parallelizing it using JobGroups
  *******************************************************************************/
 
 package org.eclipse.search.core.text;
@@ -32,6 +33,14 @@
  * even if no match can be found.
  * </p>
  * <p>
+ * {@link TextSearchEngine#search(TextSearchScope, TextSearchRequestor, java.util.regex.Pattern,
+ * org.eclipse.core.runtime.IProgressMonitor)} can perform parallel processing.
+ * To support parallel processing, subclasses of this class must synchronize access
+ * to any shared data accumulated by or accessed by overrides of the {@link #acceptFile(IFile)},
+ * {@link #reportBinaryFile(IFile)} and {@link #acceptPatternMatch(TextSearchMatchAccess)}
+ * methods, and override the {@link #canRunInParallel()} method to return true.
+ * </p>
+ * <p>
  * The order of the search results is unspecified and may vary from request to request;
  * when displaying results, clients should not rely on the order but should instead arrange the results
  * in an order that would be more meaningful to the user.
@@ -75,6 +84,10 @@
 	 * <p>
 	 * The default behaviour is to search the file for matches.
 	 * </p>
+	 * <p>
+	 * If {@link #canRunInParallel()} returns true, this method may be called in parallel by different threads,
+	 * so any access or updates to collections of results or other shared state must be synchronized.
+	 * </p>
 	 * @param file the file resource to be searched.
 	 * @return If false, no pattern matches will be reported for the content of this file.
 	 * @throws CoreException implementors can throw a {@link CoreException} if accessing the resource fails or another
@@ -94,6 +107,10 @@
 	 * reported for this file with {@link #acceptPatternMatch(TextSearchMatchAccess)}.
 	 * </p>
 	 * <p>
+	 * If {@link #canRunInParallel()} returns true, this method may be called in parallel by different threads,
+	 * so any access or updates to collections of results or other shared state must be synchronized.
+	 * </p>
+	 * <p>
 	 * The default behaviour is to skip binary files
 	 * </p>
 	 *
@@ -106,6 +123,10 @@
 
 	/**
 	 * Accepts the given search match and decides if the search should continue for this file.
+	 * <p>
+	 * If {@link #canRunInParallel()} returns true, this method may be called in parallel by different threads,
+	 * so any access or updates to collections of results or other shared state must be synchronized.
+	 * </p>
 	 *
 	 * @param matchAccess gives access to information of the match found. The matchAccess is not a value
 	 * object. Its value might change after this method is finished, and the element might be reused.
@@ -117,4 +138,22 @@
 		return true;
 	}
 
+	/**
+	 * Reports whether this TextSearchRequestor supports executing the text search algorithm
+	 * in parallel.
+	 * <p>
+	 * Subclasses should override this method and return true if they desire faster search results
+	 * and their {@link #acceptFile(IFile)}, {@link #reportBinaryFile(IFile)} and
+	 * {@link #acceptPatternMatch(TextSearchMatchAccess)} methods are thread-safe.
+	 * </p>
+	 * <p>
+	 * The default behavior is to not use parallelism when running a text search.
+	 * </p>
+	 *
+	 * @return If true, the text search will be run in parallel.
+	 * @since 3.10
+	 */
+	public boolean canRunInParallel() {
+		return false;
+	}
 }
diff --git a/org.eclipse.search/pom.xml b/org.eclipse.search/pom.xml
index 0b5d13a..b07c2ab 100644
--- a/org.eclipse.search/pom.xml
+++ b/org.eclipse.search/pom.xml
@@ -18,6 +18,6 @@
   </parent>
   <groupId>org.eclipse.search</groupId>
   <artifactId>org.eclipse.search</artifactId>
-  <version>3.9.200-SNAPSHOT</version>
+  <version>3.10.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 </project>
diff --git a/org.eclipse.search/search/org/eclipse/search/internal/core/text/TextSearchVisitor.java b/org.eclipse.search/search/org/eclipse/search/internal/core/text/TextSearchVisitor.java
index 4464e3c..8ee4d3b 100644
--- a/org.eclipse.search/search/org/eclipse/search/internal/core/text/TextSearchVisitor.java
+++ b/org.eclipse.search/search/org/eclipse/search/internal/core/text/TextSearchVisitor.java
@@ -214,8 +214,12 @@
 		fNumberOfScannedFiles= 0;
 		fNumberOfFilesToScan= files.length;
 		fCurrentFile= null;
-		int jobCount = Math.round((files.length + FILES_PER_JOB - 1) / FILES_PER_JOB);
-		final JobGroup jobGroup= new TextSearchJobGroup("Text Search", NUMBER_OF_LOGICAL_THREADS, jobCount); //$NON-NLS-1$
+		int maxThreads= fCollector.canRunInParallel() ? NUMBER_OF_LOGICAL_THREADS : 1;
+		int jobCount= 1;
+		if (maxThreads > 1) {
+			jobCount= Math.round((files.length + FILES_PER_JOB - 1) / FILES_PER_JOB);
+		}
+		final JobGroup jobGroup= new TextSearchJobGroup("Text Search", maxThreads, jobCount); //$NON-NLS-1$
 		long startTime= TRACING ? System.currentTimeMillis() : 0;
 
 		Job monitorUpdateJob= new Job(SearchMessages.TextSearchVisitor_progress_updating_job) {
diff --git a/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileSearchQuery.java b/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileSearchQuery.java
index 6204333..6ab5a30 100644
--- a/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileSearchQuery.java
+++ b/org.eclipse.search/search/org/eclipse/search/internal/ui/text/FileSearchQuery.java
@@ -63,6 +63,10 @@
 
 		}
 
+		public boolean canRunInParallel() {
+			return true;
+		}
+
 		public boolean acceptFile(IFile file) throws CoreException {
 			if (fIsLightweightAutoRefresh && !file.exists())
 				return false;