blob: 608143eef409e82efddc410278a774b52d93de30 [file] [log] [blame]
/*******************************************************************************
* 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;
}
}