| /******************************************************************************* |
| * Copyright (c) 2013, 2010 Pivotal Software, Inc. and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.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.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.jface.text.IRegion; |
| |
| /** |
| * Represents something you can search for with a 'quick search' text searcher. |
| * |
| * @author Kris De Volder |
| */ |
| public class QuickTextQuery { |
| |
| //TODO: delete and use jface Region class instead. |
| public static class TextRange implements IRegion { |
| public final int start; |
| public final int len; |
| public TextRange(int start, int len) { |
| this.start = start; |
| this.len = len; |
| } |
| @Override |
| public int getLength() { |
| return len; |
| } |
| @Override |
| public int getOffset() { |
| return start; |
| } |
| } |
| |
| private boolean caseInsensitive; |
| private String orgPattern; //Original pattern case preserved even if search is case insensitive. |
| private Pattern pattern; |
| |
| /** |
| * A query that matches anything. |
| */ |
| public QuickTextQuery() { |
| this("", true); //$NON-NLS-1$ |
| } |
| |
| public QuickTextQuery(String substring, boolean caseInsensitive) { |
| this.orgPattern = substring; |
| this.caseInsensitive = caseInsensitive; |
| createMatcher(substring, caseInsensitive); |
| } |
| |
| /** |
| * Compile a pattern string into an RegExp and create a Matcher for that |
| * regexp. This is so we can 'compile' the pattern once and then keep reusing |
| * the matcher or compiled pattern. |
| */ |
| private void createMatcher(String patString, boolean caseSensitive) { |
| StringBuilder segment = new StringBuilder(); //Accumulates text that needs to be 'quoted' |
| StringBuilder regexp = new StringBuilder(); //Accumulates 'compiled' pattern |
| int pos = 0, len = patString.length(); |
| while (pos<len) { |
| char c = patString.charAt(pos++); |
| switch (c) { |
| case '?': |
| appendSegment(segment, regexp); |
| regexp.append('.'); |
| break; |
| case '*': |
| appendSegment(segment, regexp); |
| regexp.append(".*"); //$NON-NLS-1$ |
| break; |
| case '\\': |
| if (pos<len) { |
| char nextChar = patString.charAt(pos); |
| if (nextChar=='*' || nextChar=='?' || nextChar=='\\') { |
| segment.append(nextChar); |
| pos++; |
| break; |
| } |
| } |
| default: |
| //Char is 'nothing special'. Add it to segment that will be wrapped in 'quotes' |
| segment.append(c); |
| break; |
| } |
| } |
| //Don't forget to process that last segment. |
| appendSegment(segment, regexp); |
| |
| this.pattern = Pattern.compile(regexp.toString(), caseSensitive? Pattern.CASE_INSENSITIVE : 0); |
| // this.matcher = pattern.matcher(""); |
| } |
| |
| private void appendSegment(StringBuilder segment, StringBuilder regexp) { |
| if (segment.length()>0) { |
| regexp.append(Pattern.quote(segment.toString())); |
| segment.setLength(0); //clear: ready for next segment |
| } |
| //else { |
| // nothing to append |
| //} |
| } |
| |
| public boolean equalsFilter(QuickTextQuery o) { |
| //TODO: actually for case insensitive matches we could relax this and treat patterns that |
| // differ only in case as the same. |
| return this.caseInsensitive == o.caseInsensitive && this.orgPattern.equals(o.orgPattern); |
| } |
| |
| /** |
| * Returns true if the other query is a specialisation of this query. I.e. any results matching the other |
| * query must also match this query. If this method returns true then we can optimise the search for other |
| * re-using already found results for this query. |
| * <p> |
| * If it is hard or impossible to decide whether other query is a specialisation of this query then this |
| * method is allowed to 'punt' and just return false. However, the consequence of this is that the query |
| * will be re-run instead of incrementally updated. |
| */ |
| public boolean isSubFilter(QuickTextQuery other) { |
| if (this.isTrivial()) { |
| return false; |
| } |
| if (this.caseInsensitive==other.caseInsensitive) { |
| boolean caseSensitive = this.caseInsensitive; |
| String otherPat = normalize(other.orgPattern, caseSensitive); |
| String thisPat = normalize(this.orgPattern, caseSensitive); |
| return otherPat.contains(thisPat); |
| } |
| return false; |
| } |
| |
| /** |
| * Transforms a pattern string so we can use a simple 'substring' test to determine |
| * whether one pattern is sub-pattern of the other. |
| */ |
| private String normalize(String pat, boolean caseSensitive) { |
| if (pat.endsWith("\\")) { //$NON-NLS-1$ |
| pat = pat + "\\"; //$NON-NLS-1$ |
| } |
| if (!caseSensitive) { |
| pat = pat.toLowerCase(); |
| } |
| return pat; |
| } |
| |
| /** |
| * @return whether the LineItem text contains the search pattern. |
| */ |
| public boolean matchItem(LineItem item) { |
| return matchItem(item.getText()); |
| } |
| |
| /** |
| * Same as matchItem except only takes the text of the item. This can |
| * be useful for efficient processing. In particular to avoid creating |
| * LineItem instances for non-matching lines. |
| */ |
| public boolean matchItem(String item) { |
| //Alternate implementation. This is thread safe without synchronized, |
| // but it creates some garbage. |
| Matcher matcher = pattern.matcher(item); //Creating garbage here |
| return matcher.find(); |
| } |
| |
| /** |
| * A trivial query is one that either |
| * - matches anything |
| * - matches nothing |
| * In other words, if a query is 'trivial' then it returns either nothing or all the text in the scope |
| * of the search. |
| */ |
| public boolean isTrivial() { |
| return "".equals(this.orgPattern); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public String toString() { |
| return "QTQuery("+orgPattern+", "+(caseInsensitive?"caseSens":"caseInSens")+")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ |
| } |
| |
| public List<TextRange> findAll(String text) { |
| //alternate implementation without 'synchronized' but creates more garbage |
| if (isTrivial()) { |
| return Arrays.asList(); |
| } else { |
| List<TextRange> ranges = new ArrayList<>(); |
| Matcher matcher = pattern.matcher(text); |
| while (matcher.find()) { |
| int start = matcher.start(); |
| int end = matcher.end(); |
| ranges.add(new TextRange(start, end-start)); |
| } |
| return ranges; |
| } |
| } |
| |
| public TextRange findFirst(String str) { |
| //TODO: more efficient implementation, just search the first one |
| // no need to find all matches then toss away everything except the |
| // first one. |
| List<TextRange> all = findAll(str); |
| if (all!=null && !all.isEmpty()) { |
| return all.get(0); |
| } |
| return null; |
| } |
| |
| public String getPatternString() { |
| return orgPattern; |
| } |
| |
| } |