diff --git a/addon.gradle b/addon.gradle index cc0f5d57..f7d4b042 100644 --- a/addon.gradle +++ b/addon.gradle @@ -1,3 +1,26 @@ compileJava { options.encoding = "UTF-8" } + + +generateGrammarSource { + maxHeapSize = "128m" + arguments += ['-package', 'codechicken.nei.search', '-visitor', '-no-listener'] + outputDirectory = file("${projectDir}/build/generated/sources/search/antlr/codechicken/nei/search/") +} + +compileJava.dependsOn generateGrammarSource +sourcesJar.dependsOn generateGrammarSource + +sourceSets { + main { + java.srcDirs += ["build/generated/sources/search/antlr/"] + } +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} diff --git a/build.gradle b/build.gradle index e57a16f9..5c0a3ea6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ //version: 1707058017 plugins { + id 'antlr' id 'com.gtnewhorizons.gtnhconvention' } diff --git a/dependencies.gradle b/dependencies.gradle index d037f76d..b4f7c565 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -6,5 +6,15 @@ dependencies { shadowImplementation('org.apache.commons:commons-csv:1.10.0') - compileOnly 'maven.modrinth:forge-essentials:7.5.1' + compileOnly('maven.modrinth:forge-essentials:7.5.1') + + antlr('org.antlr:antlr4:4.13.2') + shadowImplementation('org.antlr:antlr4-runtime:4.13.2') + + testImplementation(platform('org.junit:junit-bom:5.13.4')) + testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation("org.mockito:mockito-core:5.+") + + testRuntimeOnly('org.junit.platform:junit-platform-launcher') + testRuntimeOnly("org.lwjgl.lwjgl:lwjgl:2.9.3"); } diff --git a/repositories.gradle b/repositories.gradle index 9a2a141d..4e655eba 100644 --- a/repositories.gradle +++ b/repositories.gradle @@ -1,4 +1,4 @@ -// Add any additional repositiroes for your dependencies here +// Add any additional repositories for your dependencies here repositories { maven { diff --git a/src/main/antlr/SearchExpressionLexer.g4 b/src/main/antlr/SearchExpressionLexer.g4 new file mode 100644 index 00000000..64dde9b0 --- /dev/null +++ b/src/main/antlr/SearchExpressionLexer.g4 @@ -0,0 +1,45 @@ +lexer grammar SearchExpressionLexer; + +// Antlr4 generates imports with .* +@header { +// CHECKSTYLE:OFF +import codechicken.nei.SearchTokenParser; +} + +@members { +private SearchTokenParser searchParser; + +public SearchExpressionLexer(CharStream input, SearchTokenParser searchParser) { + this(input); + this.searchParser = searchParser; +} +} + +// Lexer rules +REGEX_LEFT : 'r'? '/' -> pushMode(REGEX) ; +QUOTE_LEFT : '"' -> pushMode(QUOTED) ; +DASH : '-' ; +RECIPE_INGREDIENTS : '<&' | '<' ; +RECIPE_RESULT : '>&' | '>' ; +RECIPE_OTHERS : '^&' | '^' ; +PREFIX : CLEAN_SYMBOL {searchParser.hasRedefinedPrefix(getText().charAt(0))}? ; +OR : '|' ; +LEFT_BRACKET : '{' ; +RIGHT_BRACKET : '}' ; +PLAIN_TEXT : (CLEAN_SYMBOL | ESCAPED_SYMBOL)+ {!searchParser.hasRedefinedPrefix(getText().charAt(0))}? ; +NEWLINE_OR_TAB : [\t\r\n] -> skip ; +SPACE : ' ' ; + +fragment ESCAPED_SYMBOL : '\\' . ; +fragment CLEAN_SYMBOL : ~[-<>^{}|/\\ "] ; + +mode REGEX; + +REGEX_CONTENT : (~[/] | '\\/')+ ; +REGEX_RIGHT : '/' -> popMode ; + +mode QUOTED; + +QUOTED_CONTENT : (~["] | '\\"')+ ; +QUOTE_RIGHT : '"' -> popMode ; + diff --git a/src/main/antlr/SearchExpressionParser.g4 b/src/main/antlr/SearchExpressionParser.g4 new file mode 100644 index 00000000..4abcf184 --- /dev/null +++ b/src/main/antlr/SearchExpressionParser.g4 @@ -0,0 +1,70 @@ +parser grammar SearchExpressionParser; + +// Antlr4 generates imports with .* +@header { +// CHECKSTYLE:OFF +} + +options { tokenVocab=SearchExpressionLexer; } + +// Parser rules + +// Should only be used for recipes. Thanks to antlr inability to properly +// inherit grammars, this is included where it's not supposed to be +recipeSearchExpression + : recipeClauseExpression* + ; + +recipeClauseExpression + : ingredientsPrefix=RECIPE_INGREDIENTS searchExpression[0, $ingredientsPrefix.getText().length() > 1] + | resultPrefix=RECIPE_RESULT searchExpression[1, $resultPrefix.getText().length() > 1] + | othersPrefix=RECIPE_OTHERS searchExpression[2, $othersPrefix.getText().length() > 1] + | searchExpression[3, false] + ; + +// General search expression rules +searchExpression[int type, boolean allRecipe] + : orExpression + ; + +orExpression + : sequenceExpression (OR sequenceExpression)* + ; + +sequenceExpression + : (SPACE* unaryExpression SPACE*)+ + ; + +unaryExpression + : LEFT_BRACKET orExpression RIGHT_BRACKET? + | prefixedExpression + | negateExpression + | token['\0'] + ; + +negateExpression + : DASH unaryExpression + ; + +prefixedExpression + locals [ + Character prefix + ] + : prefixToken=PREFIX { $prefix = $prefixToken.getText().charAt(0); } token[$prefix] + ; + +token[Character prefix] + : regex[prefix] + | quoted[prefix] + | PLAIN_TEXT + ; + +regex[Character prefix] + : REGEX_LEFT REGEX_CONTENT REGEX_RIGHT? + ; + +quoted[Character prefix] + : QUOTE_LEFT QUOTED_CONTENT QUOTE_RIGHT? + ; + + diff --git a/src/main/java/codechicken/nei/CollapsibleItems.java b/src/main/java/codechicken/nei/CollapsibleItems.java index 9274dc48..79edb7fc 100644 --- a/src/main/java/codechicken/nei/CollapsibleItems.java +++ b/src/main/java/codechicken/nei/CollapsibleItems.java @@ -18,12 +18,12 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; -import codechicken.nei.ItemList.AnyMultiItemFilter; -import codechicken.nei.ItemList.EverythingItemFilter; -import codechicken.nei.ItemList.NothingItemFilter; import codechicken.nei.PresetsList.Preset; import codechicken.nei.PresetsList.PresetMode; import codechicken.nei.api.ItemFilter; +import codechicken.nei.filter.AnyMultiItemFilter; +import codechicken.nei.filter.EverythingItemFilter; +import codechicken.nei.filter.NothingItemFilter; import codechicken.nei.util.ItemStackFilterParser; public class CollapsibleItems { diff --git a/src/main/java/codechicken/nei/DoubleFilteredFilter.java b/src/main/java/codechicken/nei/DoubleFilteredFilter.java deleted file mode 100644 index 6663f02d..00000000 --- a/src/main/java/codechicken/nei/DoubleFilteredFilter.java +++ /dev/null @@ -1,24 +0,0 @@ -package codechicken.nei; - -import net.minecraft.item.ItemStack; - -import codechicken.nei.api.ItemFilter; - -public class DoubleFilteredFilter implements ItemFilter { - - private ItemFilter filter; - private ItemList.PatternItemFilter patternFilter; - - public DoubleFilteredFilter(ItemFilter filter, String itemFilter) { - this.filter = filter; - this.patternFilter = new ItemList.PatternItemFilter(SearchField.getPattern(itemFilter)); - } - - @Override - public boolean matches(ItemStack item) { - if (filter.matches(item)) { - return patternFilter.matches(item); - } - return false; - } -} diff --git a/src/main/java/codechicken/nei/ItemHistoryPanel.java b/src/main/java/codechicken/nei/ItemHistoryPanel.java index f0534024..9b442c92 100644 --- a/src/main/java/codechicken/nei/ItemHistoryPanel.java +++ b/src/main/java/codechicken/nei/ItemHistoryPanel.java @@ -70,8 +70,6 @@ public class ItemHistoryPanel extends AbstractSubpanel filters; - - public AllMultiItemFilter(List filters) { - this.filters = filters; - } - - public AllMultiItemFilter(ItemFilter... filters) { - this(new LinkedList<>(Arrays.asList(filters))); - } - - public AllMultiItemFilter() { - this(new LinkedList<>()); - } - - @Override - public boolean matches(ItemStack item) { - for (ItemFilter filter : filters) try { - if (filter != null && !filter.matches(item)) return false; - } catch (Exception e) { - NEIClientConfig.logger - .error("Exception filtering " + item + " with " + filter + " (" + e.getMessage() + ")", e); - } - - return true; - } - } - - public static class AnyMultiItemFilter implements ItemFilter { - - public List filters; - - public AnyMultiItemFilter(List filters) { - this.filters = filters; - } - - public AnyMultiItemFilter() { - this(new LinkedList<>()); - } - - @Override - public boolean matches(ItemStack item) { - for (ItemFilter filter : filters) try { - if (filter != null && filter.matches(item)) return true; - } catch (Exception e) { - NEIClientConfig.logger - .error("Exception filtering " + item + " with " + filter + " (" + e.getMessage() + ")", e); - } - - return false; - } - } - public static interface ItemsLoadedCallback { public void itemsLoaded(); diff --git a/src/main/java/codechicken/nei/NEIClientConfig.java b/src/main/java/codechicken/nei/NEIClientConfig.java index e29a3b31..c1be19a9 100644 --- a/src/main/java/codechicken/nei/NEIClientConfig.java +++ b/src/main/java/codechicken/nei/NEIClientConfig.java @@ -500,12 +500,15 @@ public class NEIClientConfig { API.addOption(new OptionCycled("inventory.search.widgetAutofocus", 3, true)); tag.getTag("inventory.search.patternMode").setComment("Search Mode").getIntValue(1); - API.addOption(new OptionCycled("inventory.search.patternMode", 3, true) { + API.addOption(new OptionCycled("inventory.search.patternMode", 4, true) { @Override public boolean onClick(int button) { + if (!super.onClick(button)) { + return false; + } SearchField.searchParser.clearCache(); - return super.onClick(button); + return true; } }); @@ -575,8 +578,11 @@ public class NEIClientConfig { @Override public boolean onClick(int button) { + if (!super.onClick(button)) { + return false; + } SearchField.searchParser.clearCache(); - return super.onClick(button); + return true; } @Override @@ -600,8 +606,11 @@ public class NEIClientConfig { @Override public boolean onClick(int button) { + if (!super.onClick(button)) { + return false; + } SearchField.searchParser.clearCache(); - return super.onClick(button); + return true; } @Override @@ -624,8 +633,11 @@ public class NEIClientConfig { @Override public boolean onClick(int button) { + if (!super.onClick(button)) { + return false; + } SearchField.searchParser.clearCache(); - return super.onClick(button); + return true; } @Override @@ -648,8 +660,11 @@ public class NEIClientConfig { @Override public boolean onClick(int button) { + if (!super.onClick(button)) { + return false; + } SearchField.searchParser.clearCache(); - return super.onClick(button); + return true; } @Override @@ -672,8 +687,11 @@ public class NEIClientConfig { @Override public boolean onClick(int button) { + if (!super.onClick(button)) { + return false; + } SearchField.searchParser.clearCache(); - return super.onClick(button); + return true; } @Override @@ -691,6 +709,9 @@ public class NEIClientConfig { }); + tag.getTag("inventory.search.logSearchExceptions").setComment("Search exceptions for extended+") + .getBooleanValue(false); + String prefixRedefinitions = tag.getTag("inventory.search.prefixRedefinitions").setComment( "Redefine search prefixes by providing a char-to-char map (JSON). The keys are the original prefixes, the values the new ones. Example: { \"$\": \"€\", \"#\": \"+\", \"@\": \"°\", \"%\": \"!\" }") .getValue("{}"); @@ -701,6 +722,7 @@ public class NEIClientConfig { }; Map charMap = new Gson().fromJson(prefixRedefinitions, typeToken.getType()); SearchField.searchParser.prefixRedefinitions.putAll(charMap); + SearchField.searchParser.updateRedefinedPrefixes(); } catch (JsonParseException e) { logger.warn("Failed to convert prefix redefinitions from JSON to CharToCharMap:", e); } diff --git a/src/main/java/codechicken/nei/PresetsList.java b/src/main/java/codechicken/nei/PresetsList.java index 9c6345ab..a639728b 100644 --- a/src/main/java/codechicken/nei/PresetsList.java +++ b/src/main/java/codechicken/nei/PresetsList.java @@ -68,11 +68,11 @@ public class PresetsList { } } - protected static class RecipesFilter implements IRecipeFilterProvider, IRecipeFilter { + protected static class PresetRecipeFilter implements IRecipeFilterProvider, IRecipeFilter { public Set cache; - public IRecipeFilter getFilter() { + public IRecipeFilter getRecipeFilter() { if (cache == null) { cache = PresetsList.presets.stream().filter(p -> p.enabled && p.mode == PresetMode.REMOVE) @@ -145,7 +145,7 @@ public class PresetsList { } public static final List presets = new ArrayList<>(); - protected static RecipesFilter recipeFilter = new RecipesFilter(); + protected static PresetRecipeFilter recipeFilter = new PresetRecipeFilter(); protected static ItemPanelFilter itemFilter = new ItemPanelFilter(); static { diff --git a/src/main/java/codechicken/nei/RecipeSearchField.java b/src/main/java/codechicken/nei/RecipeSearchField.java index c22ba7a0..1393f729 100644 --- a/src/main/java/codechicken/nei/RecipeSearchField.java +++ b/src/main/java/codechicken/nei/RecipeSearchField.java @@ -3,10 +3,13 @@ package codechicken.nei; import net.minecraft.util.EnumChatFormatting; import codechicken.nei.SearchField.GuiSearchField; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.api.IRecipeFilter.IRecipeFilterProvider; import codechicken.nei.api.ItemFilter; +import codechicken.nei.api.ItemFilter.ItemFilterProvider; import codechicken.nei.util.TextHistory; -public abstract class RecipeSearchField extends TextField { +public abstract class RecipeSearchField extends TextField implements ItemFilterProvider, IRecipeFilterProvider { private static final TextHistory history = new TextHistory(); @@ -58,10 +61,16 @@ public abstract class RecipeSearchField extends TextField { return EnumChatFormatting.getTextWithoutFormattingCodes(s); } + @Override public ItemFilter getFilter() { return ((GuiSearchField) field).getFilter(); } + @Override + public IRecipeFilter getRecipeFilter() { + return ((GuiSearchField) field).getRecipeFilter(); + } + @Override public void setFocus(boolean focus) { final boolean previousFocus = field.isFocused(); diff --git a/src/main/java/codechicken/nei/SearchField.java b/src/main/java/codechicken/nei/SearchField.java index 57c11457..00e31c1c 100644 --- a/src/main/java/codechicken/nei/SearchField.java +++ b/src/main/java/codechicken/nei/SearchField.java @@ -13,11 +13,13 @@ import net.minecraft.util.EnumChatFormatting; import net.minecraftforge.fluids.FluidStack; import codechicken.lib.gui.GuiDraw; -import codechicken.nei.ItemList.NothingItemFilter; import codechicken.nei.SearchTokenParser.ISearchParserProvider; import codechicken.nei.SearchTokenParser.SearchMode; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.api.IRecipeFilter.IRecipeFilterProvider; import codechicken.nei.api.ItemFilter; import codechicken.nei.api.ItemFilter.ItemFilterProvider; +import codechicken.nei.filter.NothingItemFilter; import codechicken.nei.recipe.StackInfo; import codechicken.nei.util.TextHistory; @@ -88,7 +90,7 @@ public class SearchField extends TextField implements ItemFilterProvider { } } - public static class GuiSearchField extends FormattedTextField { + public static class GuiSearchField extends FormattedTextField implements ItemFilterProvider, IRecipeFilterProvider { protected final SearchTokenParser searchParser; @@ -104,14 +106,24 @@ public class SearchField extends TextField implements ItemFilterProvider { setPlaceholder(NEIClientUtils.translate("inventory.search.placeholder")); } + @Override public ItemFilter getFilter() { return getFilter(getText()); } + @Override + public IRecipeFilter getRecipeFilter() { + return getRecipeFilter(getText()); + } + public ItemFilter getFilter(String filterText) { return this.searchParser.getFilter(filterText); } + public IRecipeFilter getRecipeFilter(String filterText) { + return this.searchParser.getRecipeFilter(filterText); + } + } @Deprecated @@ -272,7 +284,7 @@ public class SearchField extends TextField implements ItemFilterProvider { default: break; } - + // regex and extended+ if (!search.isEmpty()) { try { return Pattern.compile(search, Pattern.MULTILINE | Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); @@ -305,6 +317,10 @@ public class SearchField extends TextField implements ItemFilterProvider { case 2: text = text.replaceAll("[{}()\\[\\].+*?^$\\\\|]", "\\\\$0"); break; + case 3: + text = text.replaceAll("\"", "\\\\$0"); + text = "\"" + text + "\""; + return text; default: break; } diff --git a/src/main/java/codechicken/nei/SearchTextFormatter.java b/src/main/java/codechicken/nei/SearchTextFormatter.java index 74705a20..ffb67033 100644 --- a/src/main/java/codechicken/nei/SearchTextFormatter.java +++ b/src/main/java/codechicken/nei/SearchTextFormatter.java @@ -7,6 +7,8 @@ import net.minecraft.util.EnumChatFormatting; import codechicken.nei.FormattedTextField.TextFormatter; import codechicken.nei.SearchTokenParser.SearchToken; +import codechicken.nei.search.SearchExpressionFormatVisitor; +import codechicken.nei.search.SearchExpressionUtils; public class SearchTextFormatter implements TextFormatter { @@ -17,50 +19,60 @@ public class SearchTextFormatter implements TextFormatter { } public String format(String text) { - final String[] parts = (text + "| ").split("\\|"); - StringJoiner formattedText = new StringJoiner(EnumChatFormatting.GRAY + "|"); + final int patternMode = NEIClientConfig.getIntSetting("inventory.search.patternMode"); + if (patternMode == 3) { + final int spaceModeEnabled = NEIClientConfig.getIntSetting("inventory.search.spaceMode"); + if (spaceModeEnabled == 1) { + text = SearchTokenParser.SPACE_PATTERN.matcher(text).replaceAll("\\\\ "); + } + final SearchExpressionFormatVisitor visitor = new SearchExpressionFormatVisitor(searchParser); + return SearchExpressionUtils.visitSearchExpression(text, visitor); + } else { + final String[] parts = (text + "| ").split("\\|"); + StringJoiner formattedText = new StringJoiner(SearchExpressionUtils.HIGHLIGHTS.OR + "|"); - for (int i = 0; i < parts.length - 1; i++) { - final String filterText = parts[i]; - final List tokens = searchParser.splitSearchText(filterText); - StringBuilder formattedPart = new StringBuilder(); - int startIndex = 0; + for (int i = 0; i < parts.length - 1; i++) { + final String filterText = parts[i]; + final List tokens = searchParser.splitSearchText(filterText); + StringBuilder formattedPart = new StringBuilder(); + int startIndex = 0; - for (SearchToken token : tokens) { - formattedPart.append(filterText.substring(startIndex, token.start)); - EnumChatFormatting tokenColor = EnumChatFormatting.RESET; + for (SearchToken token : tokens) { + formattedPart.append(filterText.substring(startIndex, token.start)); + EnumChatFormatting tokenColor = EnumChatFormatting.RESET; - if (token.firstChar != null) { - tokenColor = searchParser.getProvider(token.firstChar).getHighlightedColor(); + if (token.firstChar != null) { + tokenColor = searchParser.getProvider(token.firstChar).getHighlightedColor(); + } + + if (token.ignore) { + formattedPart.append(SearchExpressionUtils.HIGHLIGHTS.NEGATE + "-"); + } + + if (token.firstChar != null) { + formattedPart.append(tokenColor + String.valueOf(token.firstChar)); + } + + if (token.quotes) { + formattedPart.append(SearchExpressionUtils.HIGHLIGHTS.QUOTED + "\""); + } + + if (!token.rawText.isEmpty()) { + formattedPart.append(tokenColor + token.rawText); + } + + if (token.quotes) { + formattedPart.append(SearchExpressionUtils.HIGHLIGHTS.QUOTED + "\""); + } + + startIndex = token.end; } - if (token.ignore) { - formattedPart.append(EnumChatFormatting.BLUE + "-"); - } - - if (token.firstChar != null) { - formattedPart.append(tokenColor + String.valueOf(token.firstChar)); - } - - if (token.quotes) { - formattedPart.append(EnumChatFormatting.GOLD + "\""); - } - - if (!token.rawText.isEmpty()) { - formattedPart.append(tokenColor + token.rawText); - } - - if (token.quotes) { - formattedPart.append(EnumChatFormatting.GOLD + "\""); - } - - startIndex = token.end; + formattedPart.append(filterText.substring(startIndex, filterText.length())); + formattedText.add(formattedPart); } - formattedPart.append(filterText.substring(startIndex, filterText.length())); - formattedText.add(formattedPart); + return formattedText.toString(); } - - return formattedText.toString(); } } diff --git a/src/main/java/codechicken/nei/SearchTokenParser.java b/src/main/java/codechicken/nei/SearchTokenParser.java index 0d5d2bf6..25600461 100644 --- a/src/main/java/codechicken/nei/SearchTokenParser.java +++ b/src/main/java/codechicken/nei/SearchTokenParser.java @@ -4,8 +4,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -15,12 +17,17 @@ import net.minecraft.client.resources.Language; import net.minecraft.item.ItemStack; import net.minecraft.util.EnumChatFormatting; -import codechicken.nei.ItemList.AllMultiItemFilter; -import codechicken.nei.ItemList.AnyMultiItemFilter; -import codechicken.nei.ItemList.EverythingItemFilter; -import codechicken.nei.ItemList.NegatedItemFilter; -import codechicken.nei.ItemList.NothingItemFilter; +import codechicken.nei.api.IRecipeFilter; import codechicken.nei.api.ItemFilter; +import codechicken.nei.filter.AllMultiItemFilter; +import codechicken.nei.filter.AnyItemRecipeFilter; +import codechicken.nei.filter.AnyMultiItemFilter; +import codechicken.nei.filter.EverythingItemFilter; +import codechicken.nei.filter.NegatedItemFilter; +import codechicken.nei.filter.NothingItemFilter; +import codechicken.nei.search.ItemFilterVisitor; +import codechicken.nei.search.RecipeFilterVisitor; +import codechicken.nei.search.SearchExpressionUtils; public class SearchTokenParser { @@ -83,7 +90,7 @@ public class SearchTokenParser { } } - private static class IsRegisteredItemFilter implements ItemFilter { + public static class IsRegisteredItemFilter implements ItemFilter { public ItemFilter filter; @@ -99,27 +106,34 @@ public class SearchTokenParser { } } + public static final Pattern SPACE_PATTERN = Pattern.compile(" "); + protected final LRUCache filtersCache = new LRUCache<>(20); protected final List searchProviders; + protected final Set redefinedPrefixes = new HashSet<>(); protected final ProvidersCache providersCache = new ProvidersCache(); protected final Map prefixRedefinitions = new HashMap<>(); public SearchTokenParser(List searchProviders) { this.searchProviders = searchProviders; + updateRedefinedPrefixes(); } public SearchTokenParser() { this(new ArrayList<>()); + updateRedefinedPrefixes(); } public void addProvider(ISearchParserProvider provider) { this.searchProviders.add(provider); + updateRedefinedPrefixes(provider); this.providersCache.clear(); this.filtersCache.clear(); } public void clearCache() { this.filtersCache.clear(); + updateRedefinedPrefixes(); } protected List getProviders() { @@ -149,31 +163,86 @@ public class SearchTokenParser { return providersCache.providers; } + public boolean hasRedefinedPrefix(char ch) { + return redefinedPrefixes.contains(ch); + } + public ISearchParserProvider getProvider(char ch) { - return getProviders().stream() - .filter( - provider -> provider.getSearchMode() == SearchMode.PREFIX - && getRedefinedPrefix(provider.getPrefix()) == ch) - .findFirst().orElse(null); + if (!hasRedefinedPrefix(ch)) { + return null; + } + for (ISearchParserProvider provider : getProviders()) { + if (provider.getSearchMode() == SearchMode.PREFIX && getRedefinedPrefix(provider.getPrefix()) == ch) { + return provider; + } + } + return null; + } + + public List getAlwaysProvidersFilters(String searchText) { + final List filters = new ArrayList<>(); + for (ISearchParserProvider provider : getProviders()) { + if (provider.getSearchMode() == SearchMode.ALWAYS) { + filters.add(provider.getFilter(searchText)); + } + } + + return filters; } public synchronized ItemFilter getFilter(String filterText) { filterText = EnumChatFormatting.getTextWithoutFormattingCodes(filterText).toLowerCase(); + if (filterText == null || filterText.isEmpty()) { + return new EverythingItemFilter(); + } + final int spaceMode = NEIClientConfig.getIntSetting("inventory.search.spaceMode"); + final int patternMode = NEIClientConfig.getIntSetting("inventory.search.patternMode"); - return this.filtersCache.computeIfAbsent(filterText, text -> { - final String[] parts = text.split("\\|"); - final List searchTokens = Arrays.stream(parts).map(this::parseSearchText).filter(s -> s != null) - .collect(Collectors.toList()); + if (patternMode != 3) { + return this.filtersCache.computeIfAbsent(filterText, text -> { + final String[] parts = text.split("\\|"); + final List searchTokens = Arrays.stream(parts).map(this::parseSearchText) + .filter(s -> s != null).collect(Collectors.toList()); - if (searchTokens.isEmpty()) { - return new EverythingItemFilter(); - } else if (searchTokens.size() == 1) { - return new IsRegisteredItemFilter(searchTokens.get(0)); - } else { - return new IsRegisteredItemFilter(new AnyMultiItemFilter(searchTokens)); + if (searchTokens.isEmpty()) { + return new EverythingItemFilter(); + } else if (searchTokens.size() == 1) { + return new IsRegisteredItemFilter(searchTokens.get(0)); + } else { + return new IsRegisteredItemFilter(new AnyMultiItemFilter(searchTokens)); + } + }); + } else { + if (spaceMode == 1) { + filterText = SearchTokenParser.SPACE_PATTERN.matcher(filterText).replaceAll("\\\\ "); } + return this.filtersCache.computeIfAbsent(filterText, text -> { + final ItemFilterVisitor visitor = new ItemFilterVisitor(this); + final ItemFilter searchToken = SearchExpressionUtils.visitSearchExpression(text, visitor); - }); + return new IsRegisteredItemFilter(searchToken); + }); + } + + } + + public synchronized IRecipeFilter getRecipeFilter(String filterText) { + final int patternMode = NEIClientConfig.getIntSetting("inventory.search.patternMode"); + if (patternMode != 3) { + return new AnyItemRecipeFilter(getFilter(filterText)); + } + filterText = EnumChatFormatting.getTextWithoutFormattingCodes(filterText).toLowerCase(); + if (filterText == null || filterText.isEmpty()) { + return new AnyItemRecipeFilter(new EverythingItemFilter()); + } + final int spaceModeEnabled = NEIClientConfig.getIntSetting("inventory.search.spaceMode"); + + if (spaceModeEnabled == 1) { + filterText = SearchTokenParser.SPACE_PATTERN.matcher(filterText).replaceAll("\\\\ "); + } + + final RecipeFilterVisitor visitor = new RecipeFilterVisitor(this); + return SearchExpressionUtils.visitSearchExpression(filterText, visitor); } @@ -235,21 +304,33 @@ public class SearchTokenParser { } private String getPrefixes() { - StringBuilder prefixes = new StringBuilder().append('\0'); - - for (ISearchParserProvider provider : getProviders()) { - if (provider.getSearchMode() == SearchMode.PREFIX) { - prefixes.append(getRedefinedPrefix(provider.getPrefix())); - } + final StringBuilder builder = new StringBuilder(); + for (Character redefinedPrefix : redefinedPrefixes) { + builder.append(redefinedPrefix); } - - return prefixes.toString(); + return builder.toString(); } public char getRedefinedPrefix(char prefix) { return this.prefixRedefinitions.getOrDefault(prefix, prefix); } + public void updateRedefinedPrefixes() { + this.redefinedPrefixes.clear(); + for (ISearchParserProvider provider : getProviders()) { + if (provider.getSearchMode() == SearchTokenParser.SearchMode.PREFIX) { + redefinedPrefixes.add(getRedefinedPrefix(provider.getPrefix())); + } + } + this.redefinedPrefixes.add('\0'); + } + + public void updateRedefinedPrefixes(ISearchParserProvider provider) { + if (provider.getSearchMode() == SearchMode.PREFIX) { + this.redefinedPrefixes.add(getRedefinedPrefix(provider.getPrefix())); + } + } + private ItemFilter parseSearchText(String filterText) { if (filterText.isEmpty()) { diff --git a/src/main/java/codechicken/nei/SubsetWidget.java b/src/main/java/codechicken/nei/SubsetWidget.java index 6d86f914..f5843e45 100644 --- a/src/main/java/codechicken/nei/SubsetWidget.java +++ b/src/main/java/codechicken/nei/SubsetWidget.java @@ -13,6 +13,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -31,13 +32,13 @@ import com.google.gson.JsonParser; import codechicken.core.gui.GuiScrollSlot; import codechicken.lib.gui.GuiDraw; import codechicken.lib.vec.Rectangle4i; -import codechicken.nei.ItemList.AnyMultiItemFilter; -import codechicken.nei.ItemList.NothingItemFilter; import codechicken.nei.SearchTokenParser.ISearchParserProvider; import codechicken.nei.SearchTokenParser.SearchMode; import codechicken.nei.api.API; import codechicken.nei.api.ItemFilter; import codechicken.nei.api.ItemFilter.ItemFilterProvider; +import codechicken.nei.filter.AnyMultiItemFilter; +import codechicken.nei.filter.NothingItemFilter; import codechicken.nei.guihook.GuiContainerManager; import codechicken.nei.guihook.IContainerTooltipHandler; import codechicken.nei.recipe.GuiCraftingRecipe; @@ -369,12 +370,18 @@ public class SubsetWidget extends Button implements ItemFilterProvider, IContain private static class DefaultParserProvider implements ISearchParserProvider { public ItemFilter getFilter(String searchText) { - final String pathPart = searchText.replaceAll("\\s+", "").toLowerCase(); + final int patternMode = NEIClientConfig.getIntSetting("inventory.search.patternMode"); + Pattern pattern = null; + if (patternMode != 3) { + searchText = searchText.replaceAll("\\s+", "").toLowerCase(); + } else { + pattern = SearchField.getPattern(searchText, patternMode); + } final AnyMultiItemFilter filter = new AnyMultiItemFilter(); final Set filteredItems = new HashSet<>(); for (SubsetTag tag : tags.values()) { - if (tag.filter != null && tag.path.contains(pathPart)) { + if (tag.filter != null && matches(tag.path, searchText, pattern)) { filteredItems.addAll(tag.items); filter.filters.add(tag.filter); } @@ -395,6 +402,14 @@ public class SubsetWidget extends Button implements ItemFilterProvider, IContain public SearchMode getSearchMode() { return SearchMode.fromInt(NEIClientConfig.getIntSetting("inventory.search.subsetsSearchMode")); } + + private boolean matches(String name, String searchText, Pattern pattern) { + if (pattern != null) { + return pattern.matcher(name).find(); + } else { + return name.contains(searchText); + } + } } /** diff --git a/src/main/java/codechicken/nei/api/IRecipeFilter.java b/src/main/java/codechicken/nei/api/IRecipeFilter.java index bf5e7d03..bf880bb2 100644 --- a/src/main/java/codechicken/nei/api/IRecipeFilter.java +++ b/src/main/java/codechicken/nei/api/IRecipeFilter.java @@ -9,7 +9,7 @@ public interface IRecipeFilter { public static interface IRecipeFilterProvider { - public IRecipeFilter getFilter(); + public IRecipeFilter getRecipeFilter(); } public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, diff --git a/src/main/java/codechicken/nei/api/ItemInfo.java b/src/main/java/codechicken/nei/api/ItemInfo.java index 023493b9..7e8bd74c 100644 --- a/src/main/java/codechicken/nei/api/ItemInfo.java +++ b/src/main/java/codechicken/nei/api/ItemInfo.java @@ -44,8 +44,6 @@ import com.google.common.collect.ArrayListMultimap; import codechicken.core.featurehack.GameDataManipulator; import codechicken.nei.InfiniteStackSizeHandler; import codechicken.nei.InfiniteToolHandler; -import codechicken.nei.ItemList.AnyMultiItemFilter; -import codechicken.nei.ItemList.PatternItemFilter; import codechicken.nei.ItemMobSpawner; import codechicken.nei.ItemStackMap; import codechicken.nei.ItemStackSet; @@ -58,6 +56,8 @@ import codechicken.nei.config.ArrayDumper; import codechicken.nei.config.HandlerDumper; import codechicken.nei.config.ItemPanelDumper; import codechicken.nei.config.RegistryDumper; +import codechicken.nei.filter.AnyMultiItemFilter; +import codechicken.nei.filter.PatternItemFilter; import codechicken.nei.guihook.GuiContainerManager; import codechicken.nei.recipe.BrewingRecipeHandler; import codechicken.nei.recipe.RecipeItemInputHandler; diff --git a/src/main/java/codechicken/nei/config/preset/RightPanel.java b/src/main/java/codechicken/nei/config/preset/RightPanel.java index 8f52c200..f48e2004 100644 --- a/src/main/java/codechicken/nei/config/preset/RightPanel.java +++ b/src/main/java/codechicken/nei/config/preset/RightPanel.java @@ -16,9 +16,6 @@ import codechicken.core.gui.GuiWidget; import codechicken.lib.gui.GuiDraw; import codechicken.lib.vec.Rectangle4i; import codechicken.nei.CollapsibleItems; -import codechicken.nei.ItemList.AllMultiItemFilter; -import codechicken.nei.ItemList.AnyMultiItemFilter; -import codechicken.nei.ItemList.NegatedItemFilter; import codechicken.nei.ItemsGrid; import codechicken.nei.ItemsGrid.ItemsGridSlot; import codechicken.nei.NEIClientConfig; @@ -28,6 +25,9 @@ import codechicken.nei.PresetsList.Preset; import codechicken.nei.SearchField.GuiSearchField; import codechicken.nei.TextField; import codechicken.nei.api.ItemFilter; +import codechicken.nei.filter.AllMultiItemFilter; +import codechicken.nei.filter.AnyMultiItemFilter; +import codechicken.nei.filter.NegatedItemFilter; import codechicken.nei.guihook.GuiContainerManager; import codechicken.nei.guihook.IContainerTooltipHandler; import codechicken.nei.util.TextHistory; diff --git a/src/main/java/codechicken/nei/filter/AllIngredientsRecipeFilter.java b/src/main/java/codechicken/nei/filter/AllIngredientsRecipeFilter.java new file mode 100644 index 00000000..7df00d66 --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AllIngredientsRecipeFilter.java @@ -0,0 +1,23 @@ +package codechicken.nei.filter; + +import java.util.List; + +import codechicken.nei.PositionedStack; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.recipe.IRecipeHandler; + +public class AllIngredientsRecipeFilter extends AllRecipeFilter implements IRecipeFilter { + + public AllIngredientsRecipeFilter(ItemFilter filter) { + super(filter); + } + + @Override + public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, + List others) { + + return allMatch(ingredients); + } + +} diff --git a/src/main/java/codechicken/nei/filter/AllMultiItemFilter.java b/src/main/java/codechicken/nei/filter/AllMultiItemFilter.java new file mode 100644 index 00000000..f9499397 --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AllMultiItemFilter.java @@ -0,0 +1,39 @@ +package codechicken.nei.filter; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import net.minecraft.item.ItemStack; + +import codechicken.nei.NEIClientConfig; +import codechicken.nei.api.ItemFilter; + +public class AllMultiItemFilter implements ItemFilter { + + public final List filters; + + public AllMultiItemFilter(List filters) { + this.filters = filters; + } + + public AllMultiItemFilter(ItemFilter... filters) { + this(new LinkedList<>(Arrays.asList(filters))); + } + + public AllMultiItemFilter() { + this(new LinkedList<>()); + } + + @Override + public boolean matches(ItemStack item) { + for (ItemFilter filter : filters) try { + if (filter != null && !filter.matches(item)) return false; + } catch (Exception e) { + NEIClientConfig.logger + .error("Exception filtering " + item + " with " + filter + " (" + e.getMessage() + ")", e); + } + + return true; + } +} diff --git a/src/main/java/codechicken/nei/filter/AllMultiRecipeFilter.java b/src/main/java/codechicken/nei/filter/AllMultiRecipeFilter.java new file mode 100644 index 00000000..9ecfb0ad --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AllMultiRecipeFilter.java @@ -0,0 +1,42 @@ +package codechicken.nei.filter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import codechicken.nei.NEIClientConfig; +import codechicken.nei.PositionedStack; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.recipe.IRecipeHandler; + +public class AllMultiRecipeFilter implements IRecipeFilter { + + public final List filters; + + public AllMultiRecipeFilter(List filters) { + this.filters = filters; + } + + public AllMultiRecipeFilter(IRecipeFilter filters) { + this(Arrays.asList(filters)); + } + + public AllMultiRecipeFilter() { + this(new ArrayList<>()); + } + + @Override + public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, + List others) { + for (IRecipeFilter filter : filters) { + try { + if (filter != null && !filter.matches(handler, ingredients, result, others)) return false; + } catch (Exception e) { + NEIClientConfig.logger.error("Exception filtering " + handler + " with " + filter, e); + } + } + return true; + + } + +} diff --git a/src/main/java/codechicken/nei/filter/AllOthersRecipeFilter.java b/src/main/java/codechicken/nei/filter/AllOthersRecipeFilter.java new file mode 100644 index 00000000..ae76600a --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AllOthersRecipeFilter.java @@ -0,0 +1,23 @@ +package codechicken.nei.filter; + +import java.util.List; + +import codechicken.nei.PositionedStack; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.recipe.IRecipeHandler; + +public class AllOthersRecipeFilter extends AllRecipeFilter implements IRecipeFilter { + + public AllOthersRecipeFilter(ItemFilter filter) { + super(filter); + } + + @Override + public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, + List others) { + + return allMatch(others); + } + +} diff --git a/src/main/java/codechicken/nei/filter/AllRecipeFilter.java b/src/main/java/codechicken/nei/filter/AllRecipeFilter.java new file mode 100644 index 00000000..d1c507a6 --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AllRecipeFilter.java @@ -0,0 +1,41 @@ +package codechicken.nei.filter; + +import java.util.List; + +import net.minecraft.item.ItemStack; + +import codechicken.nei.PositionedStack; +import codechicken.nei.api.ItemFilter; + +public abstract class AllRecipeFilter { + + private final ItemFilter filter; + + protected AllRecipeFilter(ItemFilter filter) { + this.filter = filter; + } + + protected boolean allMatch(List items) { + for (PositionedStack pStack : items) { + if (!allMatch(pStack)) { + return false; + } + } + return true; + } + + protected boolean allMatch(PositionedStack pStack) { + if (pStack == null) { + return false; + } + + for (ItemStack stack : pStack.items) { + if (!filter.matches(stack)) { + return false; + } + } + + return true; + } + +} diff --git a/src/main/java/codechicken/nei/filter/AllSmartResultRecipeFilter.java b/src/main/java/codechicken/nei/filter/AllSmartResultRecipeFilter.java new file mode 100644 index 00000000..cdfb08fd --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AllSmartResultRecipeFilter.java @@ -0,0 +1,27 @@ +package codechicken.nei.filter; + +import java.util.List; + +import codechicken.nei.PositionedStack; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.recipe.IRecipeHandler; + +public class AllSmartResultRecipeFilter extends AllRecipeFilter implements IRecipeFilter { + + public AllSmartResultRecipeFilter(ItemFilter filter) { + super(filter); + } + + @Override + public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, + List others) { + if (result != null) { + return allMatch(result); + } else { + // GT recipes use others as a result + return allMatch(others); + } + } + +} diff --git a/src/main/java/codechicken/nei/filter/AnyIngredientsRecipeFilter.java b/src/main/java/codechicken/nei/filter/AnyIngredientsRecipeFilter.java new file mode 100644 index 00000000..e80d48c4 --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AnyIngredientsRecipeFilter.java @@ -0,0 +1,23 @@ +package codechicken.nei.filter; + +import java.util.List; + +import codechicken.nei.PositionedStack; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.recipe.IRecipeHandler; + +public class AnyIngredientsRecipeFilter extends AnyRecipeFilter implements IRecipeFilter { + + public AnyIngredientsRecipeFilter(ItemFilter filter) { + super(filter); + } + + @Override + public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, + List others) { + + return anyMatch(ingredients); + } + +} diff --git a/src/main/java/codechicken/nei/filter/AnyItemRecipeFilter.java b/src/main/java/codechicken/nei/filter/AnyItemRecipeFilter.java new file mode 100644 index 00000000..9f32f04b --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AnyItemRecipeFilter.java @@ -0,0 +1,22 @@ +package codechicken.nei.filter; + +import java.util.List; + +import codechicken.nei.PositionedStack; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.recipe.IRecipeHandler; + +public class AnyItemRecipeFilter extends AnyRecipeFilter implements IRecipeFilter { + + public AnyItemRecipeFilter(ItemFilter filter) { + super(filter); + } + + @Override + public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, + List others) { + return anyMatch(ingredients) || anyMatch(result) || anyMatch(others); + } + +} diff --git a/src/main/java/codechicken/nei/filter/AnyMultiItemFilter.java b/src/main/java/codechicken/nei/filter/AnyMultiItemFilter.java new file mode 100644 index 00000000..7fa68fb1 --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AnyMultiItemFilter.java @@ -0,0 +1,34 @@ +package codechicken.nei.filter; + +import java.util.LinkedList; +import java.util.List; + +import net.minecraft.item.ItemStack; + +import codechicken.nei.NEIClientConfig; +import codechicken.nei.api.ItemFilter; + +public class AnyMultiItemFilter implements ItemFilter { + + public final List filters; + + public AnyMultiItemFilter(List filters) { + this.filters = filters; + } + + public AnyMultiItemFilter() { + this(new LinkedList<>()); + } + + @Override + public boolean matches(ItemStack item) { + for (ItemFilter filter : filters) try { + if (filter != null && filter.matches(item)) return true; + } catch (Exception e) { + NEIClientConfig.logger + .error("Exception filtering " + item + " with " + filter + " (" + e.getMessage() + ")", e); + } + + return false; + } +} diff --git a/src/main/java/codechicken/nei/filter/AnyOthersRecipeFilter.java b/src/main/java/codechicken/nei/filter/AnyOthersRecipeFilter.java new file mode 100644 index 00000000..63e21ad4 --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AnyOthersRecipeFilter.java @@ -0,0 +1,23 @@ +package codechicken.nei.filter; + +import java.util.List; + +import codechicken.nei.PositionedStack; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.recipe.IRecipeHandler; + +public class AnyOthersRecipeFilter extends AnyRecipeFilter implements IRecipeFilter { + + public AnyOthersRecipeFilter(ItemFilter filter) { + super(filter); + } + + @Override + public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, + List others) { + + return anyMatch(others); + } + +} diff --git a/src/main/java/codechicken/nei/filter/AnyRecipeFilter.java b/src/main/java/codechicken/nei/filter/AnyRecipeFilter.java new file mode 100644 index 00000000..9fb521aa --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AnyRecipeFilter.java @@ -0,0 +1,41 @@ +package codechicken.nei.filter; + +import java.util.List; + +import net.minecraft.item.ItemStack; + +import codechicken.nei.PositionedStack; +import codechicken.nei.api.ItemFilter; + +public abstract class AnyRecipeFilter { + + private final ItemFilter filter; + + protected AnyRecipeFilter(ItemFilter filter) { + this.filter = filter; + } + + protected boolean anyMatch(List items) { + for (PositionedStack pStack : items) { + if (anyMatch(pStack)) { + return true; + } + } + return false; + } + + protected boolean anyMatch(PositionedStack pStack) { + if (pStack == null) { + return false; + } + + for (ItemStack stack : pStack.items) { + if (filter.matches(stack)) { + return true; + } + } + + return false; + } + +} diff --git a/src/main/java/codechicken/nei/filter/AnySmartResultRecipeFilter.java b/src/main/java/codechicken/nei/filter/AnySmartResultRecipeFilter.java new file mode 100644 index 00000000..b9f9c892 --- /dev/null +++ b/src/main/java/codechicken/nei/filter/AnySmartResultRecipeFilter.java @@ -0,0 +1,27 @@ +package codechicken.nei.filter; + +import java.util.List; + +import codechicken.nei.PositionedStack; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.recipe.IRecipeHandler; + +public class AnySmartResultRecipeFilter extends AnyRecipeFilter implements IRecipeFilter { + + public AnySmartResultRecipeFilter(ItemFilter filter) { + super(filter); + } + + @Override + public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, + List others) { + if (result != null) { + return anyMatch(result); + } else { + // GT recipes use others as a result + return anyMatch(others); + } + } + +} diff --git a/src/main/java/codechicken/nei/filter/DoubleItemFilter.java b/src/main/java/codechicken/nei/filter/DoubleItemFilter.java new file mode 100644 index 00000000..28862cde --- /dev/null +++ b/src/main/java/codechicken/nei/filter/DoubleItemFilter.java @@ -0,0 +1,25 @@ +package codechicken.nei.filter; + +import net.minecraft.item.ItemStack; + +import codechicken.nei.SearchField; +import codechicken.nei.api.ItemFilter; + +public class DoubleItemFilter implements ItemFilter { + + private final ItemFilter filter; + private final PatternItemFilter patternFilter; + + public DoubleItemFilter(ItemFilter filter, String itemFilter) { + this.filter = filter; + this.patternFilter = new PatternItemFilter(SearchField.getPattern(itemFilter)); + } + + @Override + public boolean matches(ItemStack item) { + if (filter.matches(item)) { + return patternFilter.matches(item); + } + return false; + } +} diff --git a/src/main/java/codechicken/nei/filter/EverythingItemFilter.java b/src/main/java/codechicken/nei/filter/EverythingItemFilter.java new file mode 100644 index 00000000..23310873 --- /dev/null +++ b/src/main/java/codechicken/nei/filter/EverythingItemFilter.java @@ -0,0 +1,13 @@ +package codechicken.nei.filter; + +import net.minecraft.item.ItemStack; + +import codechicken.nei.api.ItemFilter; + +public class EverythingItemFilter implements ItemFilter { + + @Override + public boolean matches(ItemStack item) { + return true; + } +} diff --git a/src/main/java/codechicken/nei/filter/NegatedItemFilter.java b/src/main/java/codechicken/nei/filter/NegatedItemFilter.java new file mode 100644 index 00000000..5ab036c4 --- /dev/null +++ b/src/main/java/codechicken/nei/filter/NegatedItemFilter.java @@ -0,0 +1,19 @@ +package codechicken.nei.filter; + +import net.minecraft.item.ItemStack; + +import codechicken.nei.api.ItemFilter; + +public class NegatedItemFilter implements ItemFilter { + + private final ItemFilter filter; + + public NegatedItemFilter(ItemFilter filter) { + this.filter = filter; + } + + @Override + public boolean matches(ItemStack item) { + return this.filter == null || !this.filter.matches(item); + } +} diff --git a/src/main/java/codechicken/nei/filter/NothingItemFilter.java b/src/main/java/codechicken/nei/filter/NothingItemFilter.java new file mode 100644 index 00000000..77372730 --- /dev/null +++ b/src/main/java/codechicken/nei/filter/NothingItemFilter.java @@ -0,0 +1,13 @@ +package codechicken.nei.filter; + +import net.minecraft.item.ItemStack; + +import codechicken.nei.api.ItemFilter; + +public class NothingItemFilter implements ItemFilter { + + @Override + public boolean matches(ItemStack item) { + return false; + } +} diff --git a/src/main/java/codechicken/nei/filter/PatternItemFilter.java b/src/main/java/codechicken/nei/filter/PatternItemFilter.java new file mode 100644 index 00000000..20f3c1fc --- /dev/null +++ b/src/main/java/codechicken/nei/filter/PatternItemFilter.java @@ -0,0 +1,35 @@ +package codechicken.nei.filter; + +import java.util.regex.Pattern; + +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; + +import codechicken.nei.api.ItemFilter; + +public class PatternItemFilter implements ItemFilter { + + public final Pattern pattern; + + public PatternItemFilter(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public boolean matches(ItemStack item) { + String displayName = EnumChatFormatting.getTextWithoutFormattingCodes(item.getDisplayName()); + + if (displayName != null && !displayName.isEmpty() && pattern.matcher(displayName).find()) { + return true; + } + + if (item.hasDisplayName()) { + displayName = EnumChatFormatting + .getTextWithoutFormattingCodes(item.getItem().getItemStackDisplayName(item)); + + return displayName != null && !displayName.isEmpty() && pattern.matcher(displayName).find(); + } + + return false; + } +} diff --git a/src/main/java/codechicken/nei/recipe/GuiRecipe.java b/src/main/java/codechicken/nei/recipe/GuiRecipe.java index 22ce311c..5d441675 100644 --- a/src/main/java/codechicken/nei/recipe/GuiRecipe.java +++ b/src/main/java/codechicken/nei/recipe/GuiRecipe.java @@ -43,6 +43,7 @@ import codechicken.nei.api.ItemFilter; import codechicken.nei.api.TaggedInventoryArea; import codechicken.nei.drawable.DrawableBuilder; import codechicken.nei.drawable.DrawableResource; +import codechicken.nei.filter.AllMultiRecipeFilter; import codechicken.nei.guihook.GuiContainerManager; import codechicken.nei.guihook.IContainerTooltipHandler; import codechicken.nei.guihook.IGuiClientSide; @@ -118,86 +119,6 @@ public abstract class GuiRecipe extends GuiContainer i private int yShift = 0; - public static class ItemRecipeFilter implements IRecipeFilter { - - public ItemFilter filter; - - public ItemRecipeFilter(ItemFilter filter) { - this.filter = filter; - } - - @Override - public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, - List others) { - - if (matchPositionedStack(ingredients)) { - return true; - } - - if (matchPositionedStack(result)) { - return true; - } - - if (matchPositionedStack(others)) { - return true; - } - - return false; - } - - private boolean matchPositionedStack(List items) { - for (PositionedStack pStack : items) { - if (matchPositionedStack(pStack)) { - return true; - } - } - return false; - } - - private boolean matchPositionedStack(PositionedStack pStack) { - if (pStack == null) return false; - - for (ItemStack stack : pStack.items) { - if (filter.matches(stack)) { - return true; - } - } - - return false; - } - - } - - public static class AllMultiRecipeFilter implements IRecipeFilter { - - public List filters; - - public AllMultiRecipeFilter(List filters) { - this.filters = filters; - } - - public AllMultiRecipeFilter(IRecipeFilter filters) { - this(Arrays.asList(filters)); - } - - public AllMultiRecipeFilter() { - this(new ArrayList<>()); - } - - @Override - public boolean matches(IRecipeHandler handler, List ingredients, PositionedStack result, - List others) { - for (IRecipeFilter filter : filters) { - try { - if (filter != null && !filter.matches(handler, ingredients, result, others)) return false; - } catch (Exception e) { - NEIClientConfig.logger.error("Exception filtering " + handler + " with " + filter, e); - } - } - return true; - } - } - protected static final RestartableTask updateFilter = new RestartableTask("NEI Recipe Filtering") { @Override @@ -214,7 +135,7 @@ public abstract class GuiRecipe extends GuiContainer i searchHandler.setSearchIndices(null); guiRecipe.changePage(0); } else { - final IRecipeFilter filter = new ItemRecipeFilter(GuiRecipe.searchField.getFilter()); + final IRecipeFilter filter = GuiRecipe.searchField.getRecipeFilter(); final List filtered = searchHandler.getSearchResult(filter); if (filtered == null) { @@ -546,7 +467,7 @@ public abstract class GuiRecipe extends GuiContainer i synchronized (recipeFilterers) { for (IRecipeFilterProvider p : recipeFilterers) { - IRecipeFilter filter = p.getFilter(); + IRecipeFilter filter = p.getRecipeFilter(); if (filter != null) { recipeFilter.filters.add(filter); } diff --git a/src/main/java/codechicken/nei/search/AbstractSearchExpressionVisitor.java b/src/main/java/codechicken/nei/search/AbstractSearchExpressionVisitor.java new file mode 100644 index 00000000..749d07b5 --- /dev/null +++ b/src/main/java/codechicken/nei/search/AbstractSearchExpressionVisitor.java @@ -0,0 +1,17 @@ +package codechicken.nei.search; + +import codechicken.nei.SearchTokenParser; + +public abstract class AbstractSearchExpressionVisitor extends SearchExpressionParserBaseVisitor { + + protected final SearchTokenParser searchParser; + + protected AbstractSearchExpressionVisitor(SearchTokenParser searchParser) { + this.searchParser = searchParser; + } + + public SearchTokenParser getSearchParser() { + return searchParser; + } + +} diff --git a/src/main/java/codechicken/nei/search/ItemFilterVisitor.java b/src/main/java/codechicken/nei/search/ItemFilterVisitor.java new file mode 100644 index 00000000..ea6c82e8 --- /dev/null +++ b/src/main/java/codechicken/nei/search/ItemFilterVisitor.java @@ -0,0 +1,155 @@ +package codechicken.nei.search; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.regex.Pattern; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.RuleNode; + +import codechicken.nei.NEIClientConfig; +import codechicken.nei.SearchTokenParser; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.filter.AllMultiItemFilter; +import codechicken.nei.filter.AnyMultiItemFilter; +import codechicken.nei.filter.NegatedItemFilter; +import codechicken.nei.filter.NothingItemFilter; + +public class ItemFilterVisitor extends AbstractSearchExpressionVisitor { + + private static final Pattern REGEX_ESCAPED_SPACE_PATTERN = Pattern.compile("([^\\\\](?:\\\\\\\\)+)?\\\\ "); + private static final Pattern PLAIN_TEXT_ESCAPED_PATTERN = Pattern.compile("\\\\(.)"); + private static final Pattern ESCAPED_QUOTE_PATTERN = Pattern.compile("\\\\\""); + private static final Pattern ESCAPED_SPACE_PATTERN = Pattern.compile("\\\\ "); + + public ItemFilterVisitor(SearchTokenParser searchParser) { + super(searchParser); + } + + @Override + public ItemFilter visitChildren(RuleNode node) { + if (node instanceof ParserRuleContext) { + return visitChildren((ParserRuleContext) node, null); + } else { + return defaultResult(); + } + } + + @Override + public ItemFilter visitOrExpression(SearchExpressionParser.OrExpressionContext ctx) { + return visitChildren(ctx, AnyMultiItemFilter::new); + } + + @Override + public ItemFilter visitSequenceExpression(SearchExpressionParser.SequenceExpressionContext ctx) { + return visitChildren(ctx, AllMultiItemFilter::new); + } + + @Override + public ItemFilter visitNegateExpression(SearchExpressionParser.NegateExpressionContext ctx) { + return new NegatedItemFilter(visitChildren(ctx)); + } + + @Override + public ItemFilter visitPrefixedExpression(SearchExpressionParser.PrefixedExpressionContext ctx) { + if (ctx.token() != null) { + final String cleanText = getTokenCleanText(ctx.token()); + final SearchTokenParser.ISearchParserProvider provider = searchParser.getProvider(ctx.token().prefix); + if (cleanText == null || provider == null) { + return defaultResult(); + } + return provider.getFilter(cleanText); + } + return defaultResult(); + } + + @Override + public ItemFilter visitToken(SearchExpressionParser.TokenContext ctx) { + final String cleanText = getTokenCleanText(ctx); + if (cleanText != null) { + return getAlwaysProvidersFilter(cleanText); + } + return defaultResult(); + } + + @Override + protected ItemFilter defaultResult() { + return new NothingItemFilter(); + } + + private String getTokenCleanText(SearchExpressionParser.TokenContext ctx) { + String cleanText = null; + if (ctx.PLAIN_TEXT() != null) { + cleanText = Pattern.quote( + PLAIN_TEXT_ESCAPED_PATTERN.matcher(ctx.PLAIN_TEXT().getSymbol().getText()) + // Unescape everything in search expression + .replaceAll("$1")); + } else if (ctx.regex() != null) { + if (ctx.regex().REGEX_CONTENT() != null) { + cleanText = ctx.regex().REGEX_CONTENT().getSymbol().getText(); + final int spaceModeEnabled = NEIClientConfig.getIntSetting("inventory.search.spaceMode"); + // Unescape spaces + if (spaceModeEnabled == 1) { + cleanText = REGEX_ESCAPED_SPACE_PATTERN.matcher(cleanText).replaceAll("$1 "); + } + } + } else if (ctx.quoted() != null) { + if (ctx.quoted().QUOTED_CONTENT() != null) { + cleanText = ctx.quoted().QUOTED_CONTENT().getSymbol().getText(); + // Unescape quotes + cleanText = ESCAPED_QUOTE_PATTERN.matcher(cleanText).replaceAll("\""); + cleanText = Pattern.quote(cleanText); + final int spaceModeEnabled = NEIClientConfig.getIntSetting("inventory.search.spaceMode"); + // Unescape spaces + if (spaceModeEnabled == 1) { + cleanText = ESCAPED_SPACE_PATTERN.matcher(cleanText).replaceAll(" "); + } + } + } + return cleanText; + } + + private ItemFilter visitChildren(ParserRuleContext node, Function, ItemFilter> filterConstructor) { + if (node.children != null && !node.children.isEmpty()) { + // By default return any found rule child filter (there should be only one) + if (filterConstructor == null) { + for (final ParseTree child : node.children) { + if (child instanceof RuleNode) { + return visit(child); + } + } + return defaultResult(); + // Otherwise create a filter out of rule childrens' filters + } else { + final List filters = new ArrayList<>(); + for (final ParseTree child : node.children) { + if (child instanceof RuleNode) { + filters.add(visit(child)); + } + } + return constructFilter(filters, filterConstructor); + } + } + return defaultResult(); + } + + private ItemFilter getAlwaysProvidersFilter(String searchText) { + final List filters = searchParser.getAlwaysProvidersFilters(searchText); + return constructFilter(filters, AnyMultiItemFilter::new); + } + + private ItemFilter constructFilter(List filters, Function, ItemFilter> constructor) { + if (!filters.isEmpty()) { + // Propagate the result up + if (filters.size() == 1) { + return filters.get(0); + } + return constructor.apply(filters); + } else { + return defaultResult(); + } + } + +} diff --git a/src/main/java/codechicken/nei/search/RecipeFilterVisitor.java b/src/main/java/codechicken/nei/search/RecipeFilterVisitor.java new file mode 100644 index 00000000..e32d78a2 --- /dev/null +++ b/src/main/java/codechicken/nei/search/RecipeFilterVisitor.java @@ -0,0 +1,99 @@ +package codechicken.nei.search; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import codechicken.nei.SearchTokenParser; +import codechicken.nei.api.IRecipeFilter; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.filter.AllIngredientsRecipeFilter; +import codechicken.nei.filter.AllMultiRecipeFilter; +import codechicken.nei.filter.AllOthersRecipeFilter; +import codechicken.nei.filter.AllSmartResultRecipeFilter; +import codechicken.nei.filter.AnyIngredientsRecipeFilter; +import codechicken.nei.filter.AnyItemRecipeFilter; +import codechicken.nei.filter.AnyOthersRecipeFilter; +import codechicken.nei.filter.AnySmartResultRecipeFilter; +import codechicken.nei.filter.EverythingItemFilter; + +public class RecipeFilterVisitor extends AbstractSearchExpressionVisitor { + + private final ItemFilterVisitor itemFilterVisitor; + + public RecipeFilterVisitor(SearchTokenParser searchParser) { + super(searchParser); + itemFilterVisitor = new ItemFilterVisitor(searchParser); + } + + @Override + public IRecipeFilter visitRecipeSearchExpression(SearchExpressionParser.RecipeSearchExpressionContext ctx) { + if (ctx.recipeClauseExpression() != null) { + final List filters = new ArrayList<>(); + for (SearchExpressionParser.RecipeClauseExpressionContext clauseCtx : ctx.recipeClauseExpression()) { + filters.add(createRecipeFilter(clauseCtx.searchExpression())); + } + return constructFilter(filters); + } + return defaultResult(); + } + + @Override + protected IRecipeFilter defaultResult() { + return new AnyItemRecipeFilter(new EverythingItemFilter()); + } + + private IRecipeFilter createRecipeFilter(SearchExpressionParser.SearchExpressionContext ctx) { + if (ctx == null) { + return defaultResult(); + } + final ItemFilter itemFilter = itemFilterVisitor.visitSearchExpression(ctx); + switch (ctx.type) { + case 0: + return getAllOrAnyFilter( + ctx.allRecipe, + itemFilter, + AnyIngredientsRecipeFilter::new, + AllIngredientsRecipeFilter::new); + case 1: + return getAllOrAnyFilter( + ctx.allRecipe, + itemFilter, + AnySmartResultRecipeFilter::new, + AllSmartResultRecipeFilter::new); + case 2: + return getAllOrAnyFilter( + ctx.allRecipe, + itemFilter, + AnyOthersRecipeFilter::new, + AllOthersRecipeFilter::new); + // Doesn't support all by default + case 3: + return getAllOrAnyFilter(ctx.allRecipe, itemFilter, AnyItemRecipeFilter::new, AnyItemRecipeFilter::new); + default: + return defaultResult(); + } + } + + private IRecipeFilter getAllOrAnyFilter(boolean allRecipe, ItemFilter itemFilter, + Function createAnyFilter, Function createAllFilter) { + if (allRecipe) { + return createAllFilter.apply(itemFilter); + } else { + return createAnyFilter.apply(itemFilter); + } + } + + private IRecipeFilter constructFilter(List filters) { + if (!filters.isEmpty()) { + // Propagate the result up + if (filters.size() == 1) { + return filters.get(0); + } + return new AllMultiRecipeFilter(filters); + } else { + return defaultResult(); + } + } + +} diff --git a/src/main/java/codechicken/nei/search/SearchExpressionErrorListener.java b/src/main/java/codechicken/nei/search/SearchExpressionErrorListener.java new file mode 100644 index 00000000..9425888d --- /dev/null +++ b/src/main/java/codechicken/nei/search/SearchExpressionErrorListener.java @@ -0,0 +1,18 @@ +package codechicken.nei.search; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; + +import codechicken.nei.NEIClientConfig; + +public class SearchExpressionErrorListener extends BaseErrorListener { + + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, + String msg, RecognitionException e) { + NEIClientConfig.logger + .error("NEI Search grammar exception on line " + line + " char " + charPositionInLine + ": " + msg); + } + +} diff --git a/src/main/java/codechicken/nei/search/SearchExpressionFormatVisitor.java b/src/main/java/codechicken/nei/search/SearchExpressionFormatVisitor.java new file mode 100644 index 00000000..66909b45 --- /dev/null +++ b/src/main/java/codechicken/nei/search/SearchExpressionFormatVisitor.java @@ -0,0 +1,124 @@ +package codechicken.nei.search; + +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import net.minecraft.util.EnumChatFormatting; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.RuleNode; +import org.antlr.v4.runtime.tree.TerminalNode; + +import codechicken.nei.NEIClientConfig; +import codechicken.nei.SearchTokenParser; +import codechicken.nei.SearchTokenParser.ISearchParserProvider; + +public class SearchExpressionFormatVisitor extends AbstractSearchExpressionVisitor { + + private static final Pattern REGEX_ESCAPED_SPACE_PATTERN = Pattern.compile("([^\\\\](?:\\\\\\\\)+)?\\\\ "); + private static final Pattern ESCAPED_SPACE_PATTERN = Pattern.compile("\\\\ "); + + public SearchExpressionFormatVisitor(SearchTokenParser searchParser) { + super(searchParser); + } + + @Override + public String visitChildren(RuleNode node) { + if (node instanceof ParserRuleContext) { + return visitChildren((ParserRuleContext) node, null); + } else { + return defaultResult(); + } + } + + @Override + public String visitPrefixedExpression(SearchExpressionParser.PrefixedExpressionContext ctx) { + final EnumChatFormatting format = getFormatting(ctx.prefix, null); + return format + String.valueOf(ctx.prefix) + visitToken(ctx.token()); + } + + @Override + public String visitToken(SearchExpressionParser.TokenContext ctx) { + return getTokenCleanText(ctx, ctx.prefix); + } + + @Override + public String visitRegex(SearchExpressionParser.RegexContext ctx) { + return visitChildren(ctx, ctx.prefix); + } + + @Override + public String visitQuoted(SearchExpressionParser.QuotedContext ctx) { + return visitChildren(ctx, ctx.prefix); + } + + @Override + protected String defaultResult() { + return ""; + } + + private String getTokenCleanText(SearchExpressionParser.TokenContext ctx, Character prefix) { + String cleanText = null; + final int spaceModeEnabled = NEIClientConfig.getIntSetting("inventory.search.spaceMode"); + if (ctx.PLAIN_TEXT() != null) { + cleanText = ctx.PLAIN_TEXT().getSymbol().getText(); + // Unescape spaces + if (spaceModeEnabled == 1) { + cleanText = ESCAPED_SPACE_PATTERN.matcher(cleanText).replaceAll(" "); + } + final EnumChatFormatting format = getFormatting(prefix, EnumChatFormatting.RESET); + return format + cleanText; + } else if (ctx.regex() != null) { + cleanText = visitRegex(ctx.regex()); + if (spaceModeEnabled == 1) { + cleanText = REGEX_ESCAPED_SPACE_PATTERN.matcher(cleanText).replaceAll("$1 "); + } + } else if (ctx.quoted() != null) { + cleanText = visitQuoted(ctx.quoted()); + if (spaceModeEnabled == 1) { + cleanText = ESCAPED_SPACE_PATTERN.matcher(cleanText).replaceAll(" "); + } + } + return cleanText; + } + + private String formatChild(ParseTree child, Character prefix) { + if (child instanceof TerminalNode) { + final TerminalNode node = (TerminalNode) child; + final int type = node.getSymbol().getType(); + final String format = Optional.ofNullable( + // check if highlight is defined for the token + Optional.ofNullable(SearchExpressionUtils.getHighlight(type)) + // check if highlight is defined for the prefix + .orElse(getFormatting(prefix, null)) + // use default highlight otherwise + ).map(EnumChatFormatting::toString).orElse(""); + + return format + node.getSymbol().getText(); + } else { + return visit(child); + } + } + + private String visitChildren(ParserRuleContext node, Character prefix) { + if (node.children != null && !node.children.isEmpty()) { + return node.children.stream().map(child -> formatChild(child, prefix)).collect(Collectors.joining()); + } + return defaultResult(); + } + + private EnumChatFormatting getFormatting(Character prefix, EnumChatFormatting defaultFormatting) { + if (prefix != null) { + if (prefix == '\0') { + return EnumChatFormatting.RESET; + } + final ISearchParserProvider provider = searchParser.getProvider(prefix); + if (provider != null) { + return provider.getHighlightedColor(); + } + } + return defaultFormatting; + } +} diff --git a/src/main/java/codechicken/nei/search/SearchExpressionUtils.java b/src/main/java/codechicken/nei/search/SearchExpressionUtils.java new file mode 100644 index 00000000..5e3e47b1 --- /dev/null +++ b/src/main/java/codechicken/nei/search/SearchExpressionUtils.java @@ -0,0 +1,78 @@ +package codechicken.nei.search; + +import java.util.HashMap; +import java.util.Map; + +import net.minecraft.util.EnumChatFormatting; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; + +import codechicken.nei.NEIClientConfig; +import codechicken.nei.SearchTokenParser; + +public class SearchExpressionUtils { + + private static final Map HIGHLIGHT_MAP = new HashMap<>(); + static { + for (HIGHLIGHTS elem : HIGHLIGHTS.values()) { + for (int parserType : elem.parserTypes) { + HIGHLIGHT_MAP.put(parserType, elem.f); + } + } + } + + public static enum HIGHLIGHTS { + + RECIPE(EnumChatFormatting.GREEN, SearchExpressionParser.RECIPE_INGREDIENTS, + SearchExpressionParser.RECIPE_RESULT, SearchExpressionParser.RECIPE_OTHERS), + OR(EnumChatFormatting.GRAY, SearchExpressionParser.OR), + BRACKETS(EnumChatFormatting.GRAY, SearchExpressionParser.LEFT_BRACKET, SearchExpressionParser.RIGHT_BRACKET), + NEGATE(EnumChatFormatting.BLUE, SearchExpressionParser.DASH), + REGEX(EnumChatFormatting.AQUA, SearchExpressionParser.REGEX_LEFT, SearchExpressionParser.REGEX_RIGHT), + QUOTED(EnumChatFormatting.GOLD, SearchExpressionParser.QUOTE_LEFT, SearchExpressionParser.QUOTE_RIGHT); + + public final EnumChatFormatting f; + public final int[] parserTypes; + + private HIGHLIGHTS(EnumChatFormatting format, int... parserTypes) { + this.f = format; + this.parserTypes = parserTypes; + } + + @Override + public String toString() { + return this.f.toString(); + } + } + + public static EnumChatFormatting getHighlight(Integer parserType) { + return HIGHLIGHT_MAP.get(parserType); + } + + public static T visitSearchExpression(String text, AbstractSearchExpressionVisitor visitor) { + SearchExpressionParser parser = createSearchExpressionParser(text, visitor.getSearchParser(), visitor); + return visitor.visitRecipeSearchExpression(parser.recipeSearchExpression()); + } + + private static SearchExpressionParser createSearchExpressionParser(String text, SearchTokenParser searchParser, + SearchExpressionParserBaseVisitor visitor) { + final boolean doLogExceptions = NEIClientConfig.getBooleanSetting("inventory.search.logSearchExceptions"); + final CharStream inputStream = CharStreams.fromString(text); + final SearchExpressionErrorListener errorListener = new SearchExpressionErrorListener(); + final SearchExpressionLexer lexer = new SearchExpressionLexer(inputStream, searchParser); + lexer.removeErrorListeners(); + if (doLogExceptions) { + lexer.addErrorListener(errorListener); + } + final CommonTokenStream tokenStream = new CommonTokenStream(lexer); + final SearchExpressionParser parser = new SearchExpressionParser(tokenStream); + parser.removeErrorListeners(); + if (doLogExceptions) { + parser.addErrorListener(errorListener); + } + return parser; + } + +} diff --git a/src/main/java/codechicken/nei/util/ItemStackFilterParser.java b/src/main/java/codechicken/nei/util/ItemStackFilterParser.java index cdb569ed..db12c8b1 100644 --- a/src/main/java/codechicken/nei/util/ItemStackFilterParser.java +++ b/src/main/java/codechicken/nei/util/ItemStackFilterParser.java @@ -14,11 +14,11 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraftforge.oredict.OreDictionary; -import codechicken.nei.ItemList.AllMultiItemFilter; -import codechicken.nei.ItemList.AnyMultiItemFilter; -import codechicken.nei.ItemList.NegatedItemFilter; import codechicken.nei.NEIServerUtils; import codechicken.nei.api.ItemFilter; +import codechicken.nei.filter.AllMultiItemFilter; +import codechicken.nei.filter.AnyMultiItemFilter; +import codechicken.nei.filter.NegatedItemFilter; import cpw.mods.fml.common.registry.FMLControlledNamespacedRegistry; import cpw.mods.fml.common.registry.GameData; diff --git a/src/main/resources/assets/nei/lang/en_US.lang b/src/main/resources/assets/nei/lang/en_US.lang index 9a063e8f..f413d83c 100644 --- a/src/main/resources/assets/nei/lang/en_US.lang +++ b/src/main/resources/assets/nei/lang/en_US.lang @@ -287,6 +287,7 @@ nei.options.inventory.search.patternMode=Search Mode nei.options.inventory.search.patternMode.0=Plain nei.options.inventory.search.patternMode.1=Extended nei.options.inventory.search.patternMode.2=Regex +nei.options.inventory.search.patternMode.3=Extended+ nei.options.inventory.search.spaceMode=Space Works like nei.options.inventory.search.spaceMode.0=AND nei.options.inventory.search.spaceMode.0.tip=Space between non-prefixed tokens works like "AND" diff --git a/src/test/java/codechicken/nei/SearchTokenParserTest.java b/src/test/java/codechicken/nei/SearchTokenParserTest.java new file mode 100644 index 00000000..47dd1c7e --- /dev/null +++ b/src/test/java/codechicken/nei/SearchTokenParserTest.java @@ -0,0 +1,410 @@ +package codechicken.nei; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Named.named; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.Language; +import net.minecraft.client.resources.LanguageManager; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.FieldSource; +import org.mockito.MockedStatic; + +import codechicken.nei.SearchField.SearchParserProvider; +import codechicken.nei.api.ItemFilter; +import codechicken.nei.filter.AllMultiItemFilter; +import codechicken.nei.filter.AnyMultiItemFilter; +import codechicken.nei.filter.PatternItemFilter; +import codechicken.nei.search.IdentifierFilter; + +@DisplayName("Search expression") +public class SearchTokenParserTest { + + private static class ModeValue { + + private final String name; + private final int value; + + public ModeValue(int value, String name) { + this.value = value; + this.name = name; + } + + public String getName() { + return name; + } + + public int getValue() { + return value; + } + } + + private static final ModeValue[] spaceModes = { new ModeValue(0, "space works like AND"), + new ModeValue(1, "space works like SPACE"), new ModeValue(2, "space works like AND-X") }; + private static final ModeValue[] spaceIsAndMode = { spaceModes[0] }; + private static final ModeValue[] spaceIsSpaceMode = { spaceModes[1] }; + private static final ModeValue[] patternModes = { new ModeValue(0, "Plain Text mode"), + new ModeValue(1, "Extended mode"), new ModeValue(2, "Regex mode"), new ModeValue(3, "Extended+ mode") }; + private static final ModeValue[] extendedPlusPatternMode = { patternModes[3] }; + private static final ModeValue[] extendedPatternModes = { patternModes[1], patternModes[3] }; + + private static final SearchParserProvider defaultParserProvider = parserProvider( + '\0', + "default", + SearchTokenParser.SearchMode.ALWAYS, + PatternItemFilter::new); + + private static final Function customCreateFilter = (pattern) -> new IdentifierFilter(pattern) { + + @Override + public boolean matches(ItemStack stack) { + return pattern.matcher(getStringIdentifier(stack)).find(); + } + + @Override + protected String getStringIdentifier(ItemStack stack) { + if (stack == itemA) { + return "A"; + } + return "F"; + } + + @Override + protected String getIdentifier(ItemStack stack) { + return getStringIdentifier(stack); + } + }; + + private static MockedStatic config; + private static Language language; + private static ItemStack itemA; + private static ItemStack itemB; + private static ItemStack itemC; + private static ItemStack itemAB; + private static ItemStack itemABC; + private static ItemStack itemQuestionMark; + + private static List combinationsProvider(ModeValue[] spaceModes, ModeValue[] patternModes) { + List combinations = new ArrayList<>(); + for (ModeValue s : spaceModes) { + for (ModeValue p : patternModes) { + combinations + .add(Arguments.arguments(named(s.getName(), s.getValue()), named(p.getName(), p.getValue()))); + } + } + return combinations; + } + + private static final List allModesTestProvider = combinationsProvider(spaceModes, patternModes); + + private static final List extendedPlusAllSpaceModesTestProvider = combinationsProvider( + spaceModes, + extendedPlusPatternMode); + + private static final List spaceIsSpaceAllModesTestProvider = combinationsProvider( + spaceIsSpaceMode, + patternModes); + + private static final List spaceIsAndAllModesTestProvider = combinationsProvider( + spaceIsAndMode, + patternModes); + + private static final List extendedAllSpaceModesTestProvider = combinationsProvider( + spaceModes, + extendedPatternModes); + + private static final List spaceIsAndExtendedPlusModeTestProvider = combinationsProvider( + spaceIsAndMode, + extendedPlusPatternMode); + + @BeforeAll + public static void initItems() { + itemA = mock(ItemStack.class); + when(itemA.getDisplayName()).thenReturn("a"); + itemB = mock(ItemStack.class); + when(itemB.getDisplayName()).thenReturn("b"); + itemC = mock(ItemStack.class); + when(itemC.getDisplayName()).thenReturn("c"); + itemAB = mock(ItemStack.class); + when(itemAB.getDisplayName()).thenReturn("a b"); + itemABC = mock(ItemStack.class); + when(itemABC.getDisplayName()).thenReturn("a b c"); + itemQuestionMark = mock(ItemStack.class); + when(itemQuestionMark.getDisplayName()).thenReturn("?"); + + MockedStatic staticMinecraft = mockStatic(Minecraft.class); + Minecraft minecraft = mock(Minecraft.class); + staticMinecraft.when(() -> Minecraft.getMinecraft()).thenReturn(minecraft); + LanguageManager languageManager = mock(LanguageManager.class); + when(minecraft.getLanguageManager()).thenReturn(languageManager); + language = mock(Language.class); + when(languageManager.getCurrentLanguage()).thenReturn(language); + when(language.getLanguageCode()).thenReturn("en"); + config = mockStatic(NEIClientConfig.class); + config.when(() -> NEIClientConfig.getBooleanSetting(eq("inventory.search.logSearchExceptions"))) + .thenReturn(false); + + } + + // All modes tests + + @DisplayName("No providers") + @ParameterizedTest(name = "{argumentsWithNames}") + @FieldSource("allModesTestProvider") + public void testWithoutProviders(int spaceMode, int patternMode) { + SearchTokenParser searchParser = new SearchTokenParser(); + Set matchedItems = testFilterAgainstAllItems(spaceMode, patternMode, searchParser, "test"); + + assertEquals(0, matchedItems.size()); + } + + @DisplayName("Or") + @ParameterizedTest(name = "{argumentsWithNames}") + @FieldSource("allModesTestProvider") + public void testOr(int spaceMode, int patternMode) { + SearchTokenParser searchParser = new SearchTokenParser(); + searchParser.addProvider(defaultParserProvider); + Set matchedItems = testFilterAgainstAllItems( + spaceMode, + patternMode, + searchParser, + itemA.getDisplayName() + "|" + itemC.getDisplayName()); + assertEquals(4, matchedItems.size()); + assertTrue(matchedItems.contains(itemA)); + assertTrue(matchedItems.contains(itemC)); + assertTrue(matchedItems.contains(itemAB)); + assertTrue(matchedItems.contains(itemABC)); + } + + @DisplayName("Negate") + @ParameterizedTest(name = "{argumentsWithNames}") + @FieldSource("allModesTestProvider") + public void testNegate(int spaceMode, int patternMode) { + SearchTokenParser searchParser = new SearchTokenParser(); + searchParser.addProvider(defaultParserProvider); + Set matchedItems = testFilterAgainstAllItems( + spaceMode, + patternMode, + searchParser, + "-" + itemA.getDisplayName()); + + assertEquals(3, matchedItems.size()); + assertTrue(matchedItems.contains(itemB)); + assertTrue(matchedItems.contains(itemC)); + assertTrue(matchedItems.contains(itemQuestionMark)); + } + + @DisplayName("Custom prefix provider") + @ParameterizedTest(name = "{argumentsWithNames}") + @FieldSource("allModesTestProvider") + public void testCustomPrefixProvider(int spaceMode, int patternMode) { + SearchTokenParser searchParser = new SearchTokenParser(); + searchParser.addProvider(defaultParserProvider); + searchParser + .addProvider(parserProvider('?', "custom", SearchTokenParser.SearchMode.PREFIX, customCreateFilter)); + + Set matchedItems = testFilterAgainstAllItems(spaceMode, patternMode, searchParser, "?A"); + + assertEquals(1, matchedItems.size()); + assertTrue(matchedItems.contains(itemA)); + } + + @DisplayName("Space mode") + @ParameterizedTest(name = "{argumentsWithNames}") + @FieldSource("spaceIsSpaceAllModesTestProvider") + public void testSpaceMode(int spaceMode, int patternMode) { + SearchTokenParser searchParser = new SearchTokenParser(); + searchParser.addProvider(defaultParserProvider); + Set matchedItems = testFilterAgainstAllItems( + spaceMode, + patternMode, + searchParser, + itemAB.getDisplayName()); + assertEquals(2, matchedItems.size()); + assertTrue(matchedItems.contains(itemAB)); + assertTrue(matchedItems.contains(itemABC)); + } + + @DisplayName("Spaced expression") + @ParameterizedTest(name = "{argumentsWithNames}") + @FieldSource("spaceIsAndAllModesTestProvider") + public void testSpacedExpression(int spaceMode, int patternMode) { + SearchTokenParser searchParser = new SearchTokenParser(); + searchParser.addProvider(defaultParserProvider); + Set matchedItems = testFilterAgainstAllItems( + spaceMode, + patternMode, + searchParser, + itemA.getDisplayName() + " " + itemC.getDisplayName()); + assertEquals(1, matchedItems.size()); + assertTrue(matchedItems.contains(itemABC)); + } + + // Extended modes tests + + @DisplayName("Regex") + @ParameterizedTest(name = "{argumentsWithNames}") + @FieldSource("extendedAllSpaceModesTestProvider") + public void testRegex(int spaceMode, int patternMode) { + SearchTokenParser searchParser = new SearchTokenParser(); + searchParser.addProvider(defaultParserProvider); + Set matchedItems = testFilterAgainstAllItems( + spaceMode, + patternMode, + searchParser, + "r/z?\\s" + itemB.getDisplayName() + "/"); + + assertEquals(2, matchedItems.size()); + assertTrue(matchedItems.contains(itemAB)); + assertTrue(matchedItems.contains(itemABC)); + } + + // Extended+ specific tests + + @DisplayName("Sequence") + @ParameterizedTest(name = "{argumentsWithNames}") + @FieldSource("extendedPlusAllSpaceModesTestProvider") + public void testSequence(int spaceMode, int patternMode) { + SearchTokenParser searchParser = new SearchTokenParser(); + searchParser.addProvider(defaultParserProvider); + searchParser + .addProvider(parserProvider('?', "custom", SearchTokenParser.SearchMode.PREFIX, customCreateFilter)); + + Set matchedItems = testFilterAgainstAllItems( + spaceMode, + patternMode, + searchParser, + "\"" + itemA.getDisplayName() + "\"?A"); + + assertEquals(1, matchedItems.size()); + assertTrue(matchedItems.contains(itemA)); + } + + @DisplayName("Brackets") + @ParameterizedTest(name = "{argumentsWithNames}") + @FieldSource("extendedPlusAllSpaceModesTestProvider") + public void testBrackets(int spaceMode, int patternMode) { + SearchTokenParser searchParser = new SearchTokenParser(); + searchParser.addProvider(defaultParserProvider); + Set matchedItems = testFilterAgainstAllItems( + spaceMode, + patternMode, + searchParser, + "{" + itemA.getDisplayName() + "|" + itemC.getDisplayName() + "}" + itemB.getDisplayName()); + + assertEquals(2, matchedItems.size()); + assertTrue(matchedItems.contains(itemAB)); + assertTrue(matchedItems.contains(itemABC)); + } + + @DisplayName("Quoted") + @ParameterizedTest(name = "{argumentsWithNames}") + @FieldSource("extendedPlusAllSpaceModesTestProvider") + public void testQuoted(int spaceMode, int patternMode) { + SearchTokenParser searchParser = new SearchTokenParser(); + searchParser.addProvider(defaultParserProvider); + Set matchedItems = testFilterAgainstAllItems( + spaceMode, + patternMode, + searchParser, + "\"" + itemB.getDisplayName() + " " + itemC.getDisplayName()); + + assertEquals(1, matchedItems.size()); + assertTrue(matchedItems.contains(itemABC)); + } + + @DisplayName("Escaped symbols") + @ParameterizedTest(name = "{argumentsWithNames}") + @FieldSource("spaceIsAndExtendedPlusModeTestProvider") + public void testEscapedSymbols(int spaceMode, int patternMode) { + SearchTokenParser searchParser = new SearchTokenParser(); + searchParser.addProvider(defaultParserProvider); + searchParser + .addProvider(parserProvider('?', "custom", SearchTokenParser.SearchMode.PREFIX, customCreateFilter)); + Set matchedItems = testFilterAgainstAllItems( + spaceMode, + patternMode, + searchParser, + '\\' + itemQuestionMark.getDisplayName()); + + assertEquals(1, matchedItems.size()); + assertTrue(matchedItems.contains(itemQuestionMark)); + + matchedItems = testFilterAgainstAllItems(spaceMode, patternMode, searchParser, '\\' + " "); + + assertEquals(2, matchedItems.size()); + assertTrue(matchedItems.contains(itemAB)); + assertTrue(matchedItems.contains(itemABC)); + } + + private static Set testFilterAgainstAllItems(int spaceMode, int patternMode, + SearchTokenParser searchParser, String search) { + config.when(() -> NEIClientConfig.getIntSetting(eq("inventory.search.patternMode"))).thenReturn(patternMode); + config.when(() -> NEIClientConfig.getIntSetting(eq("inventory.search.spaceMode"))).thenReturn(spaceMode); + + ItemFilter filter = searchParser.getFilter(search); + if (filter instanceof SearchTokenParser.IsRegisteredItemFilter) { + filter = ((SearchTokenParser.IsRegisteredItemFilter) filter).filter; + } + Set matchedItems = Stream.of(itemA, itemB, itemC, itemAB, itemABC, itemQuestionMark) + .filter(filter::matches).collect(Collectors.toSet()); + return matchedItems; + } + + private static SearchParserProvider parserProvider(char prefix, String name, + SearchTokenParser.SearchMode searchMode, Function createFilter) { + return new SearchParserProvider(prefix, name, EnumChatFormatting.RESET, createFilter) { + + @Override + public SearchTokenParser.SearchMode getSearchMode() { + return searchMode; + } + + @Override + public List getMatchingLanguages() { + List languages = new ArrayList<>(); + languages.add(language); + return languages; + } + + }; + } + + private static String printResult(Set items) { + return items.stream().map(ItemStack::getDisplayName).collect(Collectors.joining(",")); + } + + private static String printFilterContents(ItemFilter filter) { + if (filter instanceof AnyMultiItemFilter) { + return "ANY: (" + + ((AnyMultiItemFilter) filter).filters.stream().map(SearchTokenParserTest::printFilterContents) + .collect(Collectors.joining(",")) + + ")"; + } + if (filter instanceof AllMultiItemFilter) { + return "ALL: (" + + ((AllMultiItemFilter) filter).filters.stream().map(SearchTokenParserTest::printFilterContents) + .collect(Collectors.joining(",")) + + ")"; + } + if (filter instanceof PatternItemFilter) { + return "pattern(" + ((PatternItemFilter) filter).pattern + ")"; + } + return filter.toString(); + } +}