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