Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: e7ff3b1d5355d05e363de64abf204e00406d7d91 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
/*******************************************************************************
 * 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 class TextRange implements IRegion {
		public final int start;
		public final int len;
		public TextRange(int start, int len) {
			this.start = start;
			this.len = len;
		}
		public int getLength() {
			return len;
		}
		public int getOffset() {
			return start;
		}
	}

	private boolean caseSensitive;
	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);
	}

	public QuickTextQuery(String substring, boolean caseSensitive) {
		this.orgPattern = substring;
		this.caseSensitive = caseSensitive;
		createMatcher(substring, caseSensitive);
	}

	/**
	 * 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(".*");
				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?0:Pattern.CASE_INSENSITIVE);
//		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.caseSensitive == o.caseSensitive && 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.caseSensitive==other.caseSensitive) {
			boolean caseSensitive = this.caseSensitive;
			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("\\")) {
			pat = pat + "\\";
		}
		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);
	}

	@Override
	public String toString() {
		return "QTQuery("+orgPattern+", "+(caseSensitive?"caseSens":"caseInSens")+")";
	}

	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<QuickTextQuery.TextRange>();
			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;
	}

	public boolean isCaseSensitive() {
		return caseSensitive;
	}


}

Back to the top