1 /*******************************************************************************
2  * Copyright (c) 2013 Pivotal Software, Inc.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Eclipse Public License v2.0
5  * which accompanies this distribution, and is available at
6  * https://www.eclipse.org/legal/epl-2.0/
7  *
8  * SPDX-License-Identifier: EPL-2.0
9  *
10  * Contributors:
11  *    Pivotal Software, Inc. - initial API and implementation
12  *******************************************************************************/
13 package org.eclipse.text.quicksearch.internal.core;
14 
15 import java.io.InputStreamReader;
16 import java.util.HashSet;
17 import java.util.Iterator;
18 import java.util.Set;
19 
20 import org.eclipse.core.resources.IFile;
21 import org.eclipse.core.resources.IResource;
22 import org.eclipse.core.runtime.IProgressMonitor;
23 import org.eclipse.core.runtime.IStatus;
24 import org.eclipse.core.runtime.Status;
25 import org.eclipse.core.runtime.jobs.ISchedulingRule;
26 import org.eclipse.core.runtime.jobs.Job;
27 import org.eclipse.text.quicksearch.internal.core.pathmatch.ResourceMatcher;
28 import org.eclipse.text.quicksearch.internal.core.pathmatch.ResourceMatchers;
29 import org.eclipse.text.quicksearch.internal.core.priority.PriorityFunction;
30 import org.eclipse.text.quicksearch.internal.ui.Messages;
31 import org.eclipse.text.quicksearch.internal.util.LightSchedulingRule;
32 import org.eclipse.text.quicksearch.internal.util.LineReader;
33 
34 public class QuickTextSearcher {
35 	private final QuickTextSearchRequestor requestor;
36 	private QuickTextQuery query;
37 
38 	/**
39 	 * Keeps track of currently found matches. Items are added as they are found and may also
40 	 * be removed when the query changed and they become invalid.
41 	 */
42 	private Set<LineItem> matches = new HashSet<>(2000);
43 
44 	/**
45 	 * Scheduling rule used by Jobs that work on the matches collection.
46 	 */
47 	private ISchedulingRule matchesRule = new LightSchedulingRule("QuickSearchMatchesRule"); //$NON-NLS-1$
48 
49 	private SearchInFilesWalker walker = null;
50 	private IncrementalUpdateJob incrementalUpdate;
51 
52 	/**
53 	 * This field gets set to request a query change. The new query isn't stuffed directly
54 	 * into the query field because the query is responded to by the updater job which needs
55 	 * access to both the original query and the newQuery to decide on an efficient strategy.
56 	 */
57 	private QuickTextQuery newQuery;
58 
59 	/**
60 	 * If number of accumulated results reaches maxResults the search will be suspended.
61 	 * <p>
62 	 * Note that more results may still arrive beyond the limit since the searcher does not (yet) have the
63 	 * capability to suspend/resume a search in the middle of a file.
64 	 */
65 	private int maxResults = 200;
66 
67 	/**
68 	 * If a line of text is encountered longer than this, the searcher will stop searching
69 	 * that file (this rule avoids searching machine generated text files, like minified javascript).
70 	 */
71 	private int MAX_LINE_LEN;
72 
73 	/**
74 	 * While searching in a file, this field will be set. This can be used to show the name
75 	 * of the 'current file' in the progress area of the quicksearch dialog.
76 	 */
77 	private IFile currentFile = null;
78 
79 	/**
80 	 * Flag to disable incremental filtering logic based on incremental
81 	 * query updates. This forces a full refresh of the search results.
82 	 */
83 	private boolean forceRefresh = false;
84 	private ResourceMatcher pathMatcher = ResourceMatchers.ANY;
85 
86 	/**
87 	 * Retrieves the current result limit.
88 	 */
getMaxResults()89 	public int getMaxResults() {
90 		return maxResults;
91 	}
92 
setMaxResults(int maxResults)93 	public void setMaxResults(int maxResults) {
94 		this.maxResults = maxResults;
95 	}
96 
QuickTextSearcher(QuickTextQuery query, PriorityFunction priorities, int maxLineLen, QuickTextSearchRequestor requestor)97 	public QuickTextSearcher(QuickTextQuery query, PriorityFunction priorities, int maxLineLen, QuickTextSearchRequestor requestor) {
98 		this.MAX_LINE_LEN = maxLineLen;
99 		this.requestor = requestor;
100 		this.query = query;
101 		this.walker = createWalker(new PriorityFunction() {
102 			@Override
103 			public double priority(IResource r) {
104 				double basePriority = priorities.priority(r);
105 				if (basePriority==PRIORITY_IGNORE) {
106 					return basePriority;
107 				}
108 				if (r.getType()==IResource.FILE && !pathMatcher.matches(r)) {
109 					return PRIORITY_IGNORE;
110 				}
111 				return basePriority;
112 			}
113 		});
114 	}
115 
createWalker(PriorityFunction priorities)116 	private SearchInFilesWalker createWalker(PriorityFunction priorities) {
117 		final SearchInFilesWalker job = new SearchInFilesWalker();
118 		job.setPriorityFun(priorities);
119 		job.setRule(matchesRule);
120 		job.schedule();
121 		return job;
122 	}
123 
124 	private final class SearchInFilesWalker extends ResourceWalker {
125 
126 
127 		@Override
visit(IFile f, IProgressMonitor mon)128 		protected void visit(IFile f, IProgressMonitor mon) {
129 			if (checkCanceled(mon)) {
130 				return;
131 			}
132 
133 
134 			LineReader lr = null;
135 			currentFile = f;
136 			try {
137 				lr = new LineReader(new InputStreamReader(f.getContents(true), f.getCharset()), MAX_LINE_LEN);
138 				String line = null;
139 				int lineIndex = 1;
140 				while ((line = lr.readLine()) != null) {
141 					int offset = lr.getLastLineOffset();
142 					if (checkCanceled(mon)) {
143 						return;
144 					}
145 
146 					boolean found = query.matchItem(line);
147 					if (found) {
148 						LineItem lineItem = new LineItem(f, line, lineIndex, offset);
149 						add(lineItem);
150 					}
151 
152 					lineIndex++;
153 				}
154 			} catch (Exception e) {
155 			} finally {
156 				currentFile = null;
157 				if (lr != null) {
158 					lr.close();
159 				}
160 			}
161 		}
162 
163 		@Override
resume()164 		public void resume() {
165 			//Only resume if we don't already exceed the maxResult limit.
166 			if (isActive()) {
167 				super.resume();
168 			}
169 		}
170 
checkCanceled(IProgressMonitor mon)171 		private boolean checkCanceled(IProgressMonitor mon) {
172 			return mon.isCanceled();
173 		}
174 
requestMoreResults()175 		public void requestMoreResults() {
176 			int currentSize = matches.size();
177 			maxResults = Math.max(maxResults, currentSize + currentSize/10);
178 			resume();
179 		}
180 
181 	}
182 
183 	/**
184 	 * This job updates already found matches when the query is changed.
185 	 * Both the walker job and this job share the same scheduling rule so
186 	 * only one of them can be executing at the same time.
187 	 * <p>
188 	 * This is to avoid problems with concurrent modification of the
189 	 * matches collection.
190 	 */
191 	private class IncrementalUpdateJob extends Job {
IncrementalUpdateJob()192 		public IncrementalUpdateJob() {
193 			super(Messages.QuickTextSearch_updateMatchesJob);
194 			this.setRule(matchesRule);
195 			//This job isn't started automatically. It should be schedule every time
196 			// there's a 'newQuery' set by the user/client.
197 		}
198 
199 		@Override
run(IProgressMonitor monitor)200 		protected IStatus run(IProgressMonitor monitor) {
201 			QuickTextQuery nq = newQuery; //Copy into local variable to avoid
202 										  // problems if another thread changes newQuery while we
203 										  // are still mucking with it.
204 			if (!forceRefresh && query.isSubFilter(nq)) {
205 				query = nq;
206 				performIncrementalUpdate(monitor);
207 			} else {
208 				query = nq;
209 				forceRefresh = false;
210 				performRestart(monitor);
211 			}
212 			return monitor.isCanceled()?Status.CANCEL_STATUS:Status.OK_STATUS;
213 		}
214 
performIncrementalUpdate(IProgressMonitor mon)215 		private void performIncrementalUpdate(IProgressMonitor mon) {
216 			Iterator<LineItem> items = matches.iterator();
217 			while (items.hasNext() && !mon.isCanceled()) {
218 
219 				LineItem item = items.next();
220 				if (query.matchItem(item)) {
221 					//Match still valid but may need updating highlighted text in the UI:
222 					requestor.update(item);
223 				} else {
224 					items.remove();
225 					requestor.revoke(item);
226 				}
227 			}
228 			if (!mon.isCanceled()) {
229 				//Resume searching remaining files, if any.
230 				walker.resume();
231 			}
232 		}
233 
performRestart(IProgressMonitor mon)234 		private void performRestart(IProgressMonitor mon) {
235 			//walker may be null if dialog got closed already before we managed to
236 			// 'performRestart'.
237 			if (walker!=null) {
238 				//since we are inside Job here that uses same scheduling rule as walker, we
239 				//know walker is not currently executing. so walker cancel should be instantenous
240 				matches.clear();
241 				requestor.clear();
242 				walker.cancel();
243 				if (!query.isTrivial()) {
244 					walker.init(); //Reinitialize the walker work queue to its starting state
245 					walker.resume(); //Allow walker to resume when we release the scheduling rule.
246 				} else {
247 					walker.stop();
248 				}
249 			}
250 		}
251 
252 	}
253 
add(LineItem line)254 	private void add(LineItem line) {
255 		if (matches.add(line)) {
256 			requestor.add(line);
257 			if (!isActive()) {
258 				walker.suspend();
259 			}
260 		}
261 	}
262 
setQuery(QuickTextQuery newQuery, boolean force)263 	public void setQuery(QuickTextQuery newQuery, boolean force) {
264 		if (newQuery.equalsFilter(query) && !force) {
265 			return;
266 		}
267 		this.newQuery = newQuery;
268 		this.forceRefresh = true;
269 		scheduleIncrementalUpdate();
270 	}
271 
setPathMatcher(ResourceMatcher pathMatcher)272 	public void setPathMatcher(ResourceMatcher pathMatcher) {
273 		if (this.pathMatcher.equals(pathMatcher)) {
274 			return;
275 		}
276 		this.pathMatcher = pathMatcher;
277 		setQuery(query, true);
278 	}
279 
getQuery()280 	public QuickTextQuery getQuery() {
281 		//We return the newQuery as soon as it was set, even if it has not yet been effectively applied
282 		// to previously found query results. Most logical since when you call 'setQuery' you would
283 		// expect 'getQuery' to return the query you just set.
284 		return newQuery!=null ? newQuery : query;
285 	}
286 
scheduleIncrementalUpdate()287 	private synchronized void scheduleIncrementalUpdate() {
288 		walker.suspend(); //The walker must be suspended so the update job can run, they share scheduling rule
289 		 // so only one job can run at any time.
290 
291 		//Any outstanding incremental update should be canceled since the query has changed again.
292 		if (incrementalUpdate!=null) {
293 			incrementalUpdate.cancel();
294 		}
295 		incrementalUpdate = new IncrementalUpdateJob();
296 		incrementalUpdate.schedule();
297 	}
298 
isActive()299 	public boolean isActive() {
300 		// Information for the job showing 'Searching ...' if we are still searching.
301 		// The walker can be suspended for different reasons, not all of them count as
302 		// inactive. The main situation where the walker is suspended and interpreted as
303 		// inactive is when the max number of results are reached.
304 		return !isDone() && matches.size() < maxResults;
305 	}
306 
isDone()307 	public boolean isDone() {
308 		//Walker can be null if job was canceled because dialog closed. But stuff like
309 		//the job that shows 'Searching ...' doesn't instantly stop and may still
310 		//be asking the incremental update job whether its done.
311 		return /*(incrementalUpdate != null && incrementalUpdate.getState() != Job.NONE) ||*/ (walker!=null && walker.isDone());
312 	}
313 
requestMoreResults()314 	public void requestMoreResults() {
315 		if (walker!=null && !walker.isDone()) {
316 			walker.requestMoreResults();
317 		}
318 	}
319 
cancel()320 	public void cancel() {
321 		if (walker!=null) {
322 			walker.cancel();
323 			walker = null;
324 		}
325 	}
326 
getCurrentFile()327 	public IFile getCurrentFile() {
328 		return currentFile;
329 	}
330 
331 }
332