/******************************************************************************* * Copyright (c) 2013 Pivotal Software, Inc. * 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: * Pivotal Software, Inc. - initial API and implementation *******************************************************************************/ package org.springsource.ide.eclipse.commons.quicksearch.core; import java.io.InputStreamReader; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.Job; import org.springsource.ide.eclipse.commons.quicksearch.core.priority.PriorityFunction; import org.springsource.ide.eclipse.commons.quicksearch.util.JobUtil; import org.springsource.ide.eclipse.commons.quicksearch.util.LineReader; public class QuickTextSearcher { private final QuickTextSearchRequestor requestor; private QuickTextQuery query; /** * Keeps track of currently found matches. Items are added as they are found and may also * be removed when the query changed and they become invalid. */ private Set<LineItem> matches = new HashSet<LineItem>(2000); /** * Scheduling rule used by Jobs that work on the matches collection. */ private ISchedulingRule matchesRule = JobUtil.lightRule("QuickSearchMatchesRule"); private SearchInFilesWalker walker = null; private IncrementalUpdateJob incrementalUpdate; /** * This field gets set to request a query change. The new query isn't stuffed directly * into the query field because the query is responded to by the updater job which needs * access to both the original query and the newQuery to decide on an efficient strategy. */ private QuickTextQuery newQuery; /** * If number of accumulated results reaches maxResults the search will be suspended. * <p> * Note that more results may still arrive beyond the limit since the searcher does not (yet) have the * capability to suspend/resume a search in the middle of a file. */ private int maxResults = 200; /** * If a line of text is encountered longer than this, the searcher will stop searching * that file (this rule avoids searching machine generated text files, like minified javascript). */ private int MAX_LINE_LEN; /** * While searching in a file, this field will be set. This can be used to show the name * of the 'current file' in the progress area of the quicksearch dialog. */ private IFile currentFile = null; /** * Retrieves the current result limit. */ public int getMaxResults() { return maxResults; } public void setMaxResults(int maxResults) { this.maxResults = maxResults; } public QuickTextSearcher(QuickTextQuery query, PriorityFunction priorities, int maxLineLen, QuickTextSearchRequestor requestor) { this.MAX_LINE_LEN = maxLineLen; this.requestor = requestor; this.query = query; this.walker = createWalker(priorities); } private SearchInFilesWalker createWalker(PriorityFunction priorities) { final SearchInFilesWalker job = new SearchInFilesWalker(); job.setPriorityFun(priorities); job.setRule(matchesRule); job.schedule(); return job; } private final class SearchInFilesWalker extends ResourceWalker { @Override protected void visit(IFile f, IProgressMonitor mon) { if (checkCanceled(mon)) { return; } LineReader lr = null; currentFile = f; try { lr = new LineReader(new InputStreamReader(f.getContents(true), f.getCharset()), MAX_LINE_LEN); String line = null; int lineIndex = 1; while ((line = lr.readLine()) != null) { int offset = lr.getLastLineOffset(); if (checkCanceled(mon)) { return; } boolean found = query.matchItem(line); if (found) { LineItem lineItem = new LineItem(f, line, lineIndex, offset); add(lineItem); } lineIndex++; } } catch (Exception e) { } finally { currentFile = null; if (lr != null) { lr.close(); } } } // @Override // protected void visit(IFile f, IProgressMonitor mon) { // if (checkCanceled(mon)) { // return; // } //// System.out.println("visit: "+f); // FileTextSearchScope scope = FileTextSearchScope.newSearchScope(new IResource[] {f}, new String[] {"*"}, false); // FileSearchQuery search = new FileSearchQuery(query.getPatternString(), false, query.isCaseSensitive(), scope); // search.run(new NullProgressMonitor()); // FileSearchResult result = (FileSearchResult) search.getSearchResult(); // for (Object el : result.getElements()) { // for (Match _match : result.getMatches(el)) { // if (checkCanceled(mon)) { // return; // } // FileMatch match = (FileMatch) _match; // LineItem line = new LineItem(match); // add(line); // } // } // } @Override public void resume() { //Only resume if we don't already exceed the maxResult limit. if (matches.size()<maxResults) { super.resume(); } } private boolean checkCanceled(IProgressMonitor mon) { return mon.isCanceled(); } public void requestMoreResults() { int currentSize = matches.size(); maxResults = Math.max(maxResults, currentSize + currentSize/10); resume(); } } /** * This job updates already found matches when the query is changed. * Both the walker job and this job share the same scheduling rule so * only one of them can be executing at the same time. * <p> * This is to avoid problems with concurrent modification of the * matches collection. */ private class IncrementalUpdateJob extends Job { public IncrementalUpdateJob() { super("Update matches"); this.setRule(matchesRule); //This job isn't started automatically. It should be schedule every time // there's a 'newQuery' set by the user/client. } @Override protected IStatus run(IProgressMonitor monitor) { QuickTextQuery nq = newQuery; //Copy into local variable to avoid // problems if another thread changes newQuery while we // are still mucking with it. if (query.isSubFilter(nq)) { query = nq; performIncrementalUpdate(monitor); } else { query = nq; performRestart(monitor); } return monitor.isCanceled()?Status.CANCEL_STATUS:Status.OK_STATUS; } private void performIncrementalUpdate(IProgressMonitor mon) { Iterator<LineItem> items = matches.iterator(); while (items.hasNext() && !mon.isCanceled()) { LineItem item = items.next(); if (query.matchItem(item)) { //Match still valid but may need updating highlighted text in the UI: requestor.update(item); } else { items.remove(); requestor.revoke(item); } } if (!mon.isCanceled()) { //Resume searching remaining files, if any. walker.resume(); } } private void performRestart(IProgressMonitor mon) { //walker may be null if dialog got closed already before we managed to // 'performRestart'. if (walker!=null) { //since we are inside Job here that uses same scheduling rule as walker, we //know walker is not currently executing. so walker cancel should be instantenous matches.clear(); requestor.clear(); walker.cancel(); if (!query.isTrivial()) { walker.init(); //Reinitialize the walker work queue to its starting state walker.resume(); //Allow walker to resume when we release the scheduling rule. } } } } private void add(LineItem line) { if (matches.add(line)) { requestor.add(line); if (matches.size() >= maxResults) { walker.suspend(); } } } public void setQuery(QuickTextQuery newQuery) { if (newQuery.equalsFilter(query)) { return; } this.newQuery = newQuery; walker.suspend(); //The walker must be suspended so the update job can run, they share scheduling rule // so only one job can run at any time. scheduleIncrementalUpdate(); } public QuickTextQuery getQuery() { //We return the newQuery as soon as it was set, even if it has not yet been effectively applied // to previously found query results. Most logical since when you call 'setQuery' you would // expect 'getQuery' to return the query you just set. return newQuery!=null ? newQuery : query; } private synchronized void scheduleIncrementalUpdate() { //Any outstanding incremental update should be canceled since the query has changed again. if (incrementalUpdate!=null) { incrementalUpdate.cancel(); } incrementalUpdate = new IncrementalUpdateJob(); incrementalUpdate.schedule(); } public boolean isDone() { //Walker can be null if job was canceled because dialog closed. But stuff like //the job that shows 'Searching ...' doesn't instantly stop and may still //be asking the incremental update job whether its done. return walker!=null && walker.isDone(); } public void requestMoreResults() { if (walker!=null && !walker.isDone()) { walker.requestMoreResults(); } } public void cancel() { if (walker!=null) { walker.cancel(); walker = null; } } public IFile getCurrentFile() { return currentFile; } }