Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More efficient Regex search with improved user experience. #12775

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 59 additions & 25 deletions vassal-app/src/main/java/VASSAL/configure/ConfigureTree.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.IntStream;

/**
Expand Down Expand Up @@ -2090,6 +2091,7 @@ private static class SearchAction extends AbstractAction {

private final ConfigureTree configureTree;
private final SearchParameters searchParameters;
private Pattern regexPattern;

/**
* Constructs a new {@link SearchAction}
Expand Down Expand Up @@ -2176,11 +2178,23 @@ public void actionPerformed(ActionEvent e) {
ConfigureTree.chat(Resources.getString("Editor.search_all_off"));
}

if (!searchParameters.getSearchString().isEmpty() && (!searchParameters.isMatchRegex() || isValidRegex(searchParameters.getSearchString()))) {
if (!searchParameters.getSearchString().isEmpty()) {
if (anyChanges) {
// Unless we're just continuing to the next match in an existing search, compute & display hit count
final int matches = getNumMatches(searchParameters.getSearchString());
chat(matches + " " + Resources.getString("Editor.search_count") + noHTML(searchParameters.getSearchString()));
boolean regexError = Boolean.FALSE;
// Unless we're just continuing to the next match in an existing search, setup.
if (searchParameters.isMatchRegex()) {
regexPattern = setupRegexSearch(searchParameters.getSearchString());
regexError = regexPattern == null;
}
else {
regexPattern = null;
}
if (!regexError) {
// Compute & display hit count as heading, no indent
final int matches = getNumMatches(searchParameters.getSearchString());
// FIXME: For some reason leading spaces now being stripped from Resource strings, hence added here
chatter.show(matches + " " + (regexPattern == null ? Resources.getString("Editor.search_count") : Resources.getString("Editor.search_countRegex")) + ": " + noHTML(regexPattern == null ? searchParameters.getSearchString() : regexPattern.toString()));
}
}

// Find first match
Expand All @@ -2196,7 +2210,8 @@ public void actionPerformed(ActionEvent e) {
}
}
else {
chat(Resources.getString("Editor.search_none_found") + noHTML(searchParameters.getSearchString()));
// No need to display this on first pass, as we already said zero found.
if (!anyChanges) chat(regexPattern == null ? Resources.getString("Editor.search_none_found") + noHTML(searchParameters.getSearchString()) : Resources.getString("Editor.search_noRegex_match") + noHTML(regexPattern.toString()));
}
}
});
Expand Down Expand Up @@ -2327,7 +2342,7 @@ private int getNumMatches(String searchString) {

/**
* @param st - Search target (usually Decorator or AbstractConfigurable)
* @param searchString - our search string
* @param searchString - our search string unless superseded by regexPattern
* @return true if the node matches our searchString based on search configuration ("match" checkboxes)
*/
private boolean checkSearchTarget(SearchTarget st, String searchString) {
Expand Down Expand Up @@ -2427,7 +2442,7 @@ private boolean checkNode(DefaultMutableTreeNode node, String searchString) {
return false;
}

// From here down we are only searching inside of SearchTarget objects (Piece/Prototypes, or searchable AbstractConfigurables)
// From here down we are only searching inside SearchTarget objects (Piece/Prototypes, or searchable AbstractConfigurables)
GamePiece p;
boolean protoskip;
if (c instanceof GamePiece) {
Expand Down Expand Up @@ -2654,39 +2669,58 @@ else if (c instanceof PrototypeDefinition) {
}
}

private boolean isValidRegex(String searchString) { // avoid exceptions by checking the Regex before use
try {
return "".matches(searchString) || true;
}
catch (java.util.regex.PatternSyntaxException e) {
chat("Search string is not a valid Regular Expression: " + e.getMessage()); //NON-NLS
return false;
}
}

/**
* Checks a single string against our search parameters
* @param target - string to check
* @param searchString - our search string
* @param searchString - our search string - unless superseded by regexPattern
* @return true if this is a match based on our "matchCase" & "matchRegex"checkboxes.
*/
private boolean checkString(String target, String searchString) {
if (searchParameters.isMatchRegex()) {
if (regexPattern == null) {
if (searchParameters.isMatchCase()) {
return target.matches(searchString);
return target.contains(searchString);
}
else {
return target.toLowerCase().matches(searchString.toLowerCase());
return target.toLowerCase().contains(searchString.toLowerCase());
}
}
else {
if (searchParameters.isMatchCase()) {
return target.contains(searchString);
// Regular Expression check - match on pattern established in setupRegexPattern()
return regexPattern.matcher(target).find();
}
}

/**
* Initialise a Pattern for subsequent Matcher / Matches
* @param searchString - Regex search string
* @return Pattern for searches, with an applied default
*/
private Pattern setupRegexSearch(String searchString) {

final String caseModifier = (!searchParameters.isMatchCase() ? "" : "(?i)");
String regexSearchString = caseModifier + searchString;

// If the string contains no Regex operands, establish a useful default
// FIXME: escape characters will be interpreted as regex, bypassing the default. This may be ok anyway.
if (!searchString.matches("\\[a-zA-Z]|\\*|\\.|\\?|\\^|\\$|\\(.*?\\)|\\[.*?\\]|\\{.*?\\}|\\|")) {
try {
Pattern.compile(".*\\b" + regexSearchString + ".*?"); // test default
regexSearchString = ".*\\b" + regexSearchString + ".*?"; // accept default
chat(Resources.getString("Editor.search_regexDefault") + noHTML(searchString)); // NON-NLS
}
else {
return target.toLowerCase().contains(searchString.toLowerCase());
catch (java.util.regex.PatternSyntaxException e) {
// tolerate the exception and carry on
logger.warn("Pattern Syntax Error in default Editor Search: " + noHTML(e.getMessage())); //NON-NLS
}
}

try {
return Pattern.compile(regexSearchString);
}
catch (java.util.regex.PatternSyntaxException e) {
chat("Search string is not a valid Regular Expression: " + noHTML(e.getMessage())); //NON-NLS
return null;
}
}
}

Expand Down
5 changes: 4 additions & 1 deletion vassal-app/src/main/resources/VASSAL/i18n/Editor.properties
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Editor.search_string=String to find
Editor.search_next=Find next
Editor.search_case=Exact case
Editor.search_regex=Regular Expression
Editor.search_regexDefault=No Regex special characters detected; searching for phrase:
Editor.search_advanced=Advanced search
Editor.search_names=Match names
Editor.search_types=Match [Class names]
Expand All @@ -83,7 +84,9 @@ Editor.search_menus=Match UI text
Editor.search_messages=Match message formats
Editor.search_all_off=Warning - all field flags unchecked - turning on 'Match Names'
Editor.search_none_found=Search string not found:
Editor.search_count= Matches found for search string:
Editor.search_noRegex_match=No match found for Regular Expression:
Editor.search_count=matches found for search string
Editor.search_countRegex=matches found for Regular Expression
Editor.edit_extension=Edit extension
Editor.new_extension=New extension
Editor.cant_cut_ancestor_to_child=Can't paste a cut ancestor to its own child.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

VASSAL's Module Editor has a powerful Search facility to find components and pieces in a number of different ways.

The default search is to search for everything, but the Advanced Searh options can be used to limit the type of search performed.
The default search is to search for everything, but the Advanced Search options can be used to limit the type of search performed.

Note that a search will continue from the last item found and will wrap-around, until you modify the search criteria. This applies even across a restart of editing sessions.

'''''

Expand All @@ -22,7 +24,7 @@ a|
Force an Exact case search (i.e. abc is different to ABC).

===== Regular Expression
The specified search string is a https://en.wikipedia.org/wiki/Regular_expression[Regular Expression]. In the simplest example, a search for ABC will find matches that equate wholly to "ABC", and lower or mixed case variants (unless _Exact Case_ is checked as well).
The specified search string is a https://en.wikipedia.org/wiki/Regular_expression[Regular Expression]. If the Search string lacks Regex special characters, a pattern of the form `.\*/b<search string>.*` will be attempted. This default will find matches on words or phrases starting with the search string.

===== Advanced Search
Show the advanced search options. All search options are on in a simple search. The advanced options allow you to turn off options to narrow your search.
Expand Down