blob: 7355539545fdecd97c50b3df737599a76f2a48af [file] [log] [blame]
/*******************************************************************************
* 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 v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.text.quicksearch.internal.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.resources.IResource;
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.eclipse.text.quicksearch.internal.core.pathmatch.ResourceMatcher;
import org.eclipse.text.quicksearch.internal.core.pathmatch.ResourceMatchers;
import org.eclipse.text.quicksearch.internal.core.priority.PriorityFunction;
import org.eclipse.text.quicksearch.internal.ui.Messages;
import org.eclipse.text.quicksearch.internal.util.LightSchedulingRule;
import org.eclipse.text.quicksearch.internal.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 = new LightSchedulingRule("QuickSearchMatchesRule"); //$NON-NLS-1$
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;
/**
* Flag to disable incremental filtering logic based on incremental
* query updates. This forces a full refresh of the search results.
*/
private boolean forceRefresh = false;
private ResourceMatcher pathMatcher = ResourceMatchers.ANY;
/**
* 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(new PriorityFunction() {
@Override
public double priority(IResource r) {
double basePriority = priorities.priority(r);
if (basePriority==PRIORITY_IGNORE) {
return basePriority;
}
if (r.getType()==IResource.FILE && !pathMatcher.matches(r)) {
return PRIORITY_IGNORE;
}
return basePriority;
}
});
}
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
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(Messages.QuickTextSearch_updateMatchesJob);
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 (!forceRefresh && query.isSubFilter(nq)) {
query = nq;
performIncrementalUpdate(monitor);
} else {
query = nq;
forceRefresh = false;
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, boolean force) {
if (newQuery.equalsFilter(query) && !force) {
return;
}
this.newQuery = newQuery;
this.forceRefresh = true;
scheduleIncrementalUpdate();
}
public void setPathMatcher(ResourceMatcher pathMatcher) {
if (this.pathMatcher.equals(pathMatcher)) {
return;
}
this.pathMatcher = pathMatcher;
setQuery(query, true);
}
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() {
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.
//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;
}
}