Advanced search expressions and improved recipe search in a new pattern mode: extended+ (#692)

This commit is contained in:
sbwaz authored and GitHub committed 2025-08-24 14:35:59 +07:00
1 parent c1f7489de1
commit e5d55a9bd8
47 files changed
+1779 -321

No files matched your search

+23
View File
@@ -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"
}
}
+1
View File
@@ -1,5 +1,6 @@
//version: 1707058017
plugins {
id 'antlr'
id 'com.gtnewhorizons.gtnhconvention'
}
+11 -1
View File
@@ -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");
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Add any additional repositiroes for your dependencies here
// Add any additional repositories for your dependencies here
repositories {
maven {
+45
View File
@@ -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 ;
+70
View File
@@ -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?
;
@@ -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 {
@@ -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;
}
}
@@ -70,8 +70,6 @@ public class ItemHistoryPanel extends AbstractSubpanel<ItemsGrid<ItemsGridSlot,
final int useRows = NEIClientConfig.getIntSetting("inventory.history.useRows");
final int rows = (int) Math.min(Math.ceil(this.grid.size() * 1f / columns), useRows);
System.out.println("rows: " + rows + " " + columns + " " + this.grid.size() + " " + width);
this.w = width;
this.h = 8 + ItemsGrid.SLOT_SIZE * Math.max(rows, 1);
+1 -114
View File
@@ -1,7 +1,6 @@
package codechicken.nei;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@@ -11,14 +10,12 @@ import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import net.minecraft.client.Minecraft;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.IIcon;
import com.google.common.collect.ArrayListMultimap;
@@ -28,6 +25,7 @@ import codechicken.nei.ThreadOperationTimer.TimeoutException;
import codechicken.nei.api.ItemFilter;
import codechicken.nei.api.ItemFilter.ItemFilterProvider;
import codechicken.nei.api.ItemInfo;
import codechicken.nei.filter.AllMultiItemFilter;
import codechicken.nei.recipe.InformationHandler;
import codechicken.nei.search.TooltipFilter;
@@ -55,117 +53,6 @@ public class ItemList {
*/
public static boolean loadFinished;
public static class EverythingItemFilter implements ItemFilter {
@Override
public boolean matches(ItemStack item) {
return true;
}
}
public static class NothingItemFilter implements ItemFilter {
@Override
public boolean matches(ItemStack item) {
return false;
}
}
public static class NegatedItemFilter implements ItemFilter {
public ItemFilter filter;
public NegatedItemFilter(ItemFilter filter) {
this.filter = filter;
}
@Override
public boolean matches(ItemStack item) {
return this.filter == null || !this.filter.matches(item);
}
}
public static class PatternItemFilter implements ItemFilter {
public 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;
}
}
public static class AllMultiItemFilter implements ItemFilter {
public List<ItemFilter> filters;
public AllMultiItemFilter(List<ItemFilter> 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<ItemFilter> filters;
public AnyMultiItemFilter(List<ItemFilter> 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();
@@ -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<Character, Character> 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);
}
@@ -68,11 +68,11 @@ public class PresetsList {
}
}
protected static class RecipesFilter implements IRecipeFilterProvider, IRecipeFilter {
protected static class PresetRecipeFilter implements IRecipeFilterProvider, IRecipeFilter {
public Set<String> 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<Preset> presets = new ArrayList<>();
protected static RecipesFilter recipeFilter = new RecipesFilter();
protected static PresetRecipeFilter recipeFilter = new PresetRecipeFilter();
protected static ItemPanelFilter itemFilter = new ItemPanelFilter();
static {
@@ -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();
+19 -3
View File
@@ -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;
}
@@ -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<SearchToken> 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<SearchToken> 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();
}
}
@@ -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<String, ItemFilter> filtersCache = new LRUCache<>(20);
protected final List<ISearchParserProvider> searchProviders;
protected final Set<Character> redefinedPrefixes = new HashSet<>();
protected final ProvidersCache providersCache = new ProvidersCache();
protected final Map<Character, Character> prefixRedefinitions = new HashMap<>();
public SearchTokenParser(List<ISearchParserProvider> 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<ISearchParserProvider> 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<ItemFilter> getAlwaysProvidersFilters(String searchText) {
final List<ItemFilter> 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<ItemFilter> 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<ItemFilter> 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()) {
@@ -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<ItemStack> 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);
}
}
}
/**
@@ -9,7 +9,7 @@ public interface IRecipeFilter {
public static interface IRecipeFilterProvider {
public IRecipeFilter getFilter();
public IRecipeFilter getRecipeFilter();
}
public boolean matches(IRecipeHandler handler, List<PositionedStack> ingredients, PositionedStack result,
@@ -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;
@@ -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;
@@ -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<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> others) {
return allMatch(ingredients);
}
}
@@ -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<ItemFilter> filters;
public AllMultiItemFilter(List<ItemFilter> 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;
}
}
@@ -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<IRecipeFilter> filters;
public AllMultiRecipeFilter(List<IRecipeFilter> filters) {
this.filters = filters;
}
public AllMultiRecipeFilter(IRecipeFilter filters) {
this(Arrays.asList(filters));
}
public AllMultiRecipeFilter() {
this(new ArrayList<>());
}
@Override
public boolean matches(IRecipeHandler handler, List<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> 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;
}
}
@@ -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<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> others) {
return allMatch(others);
}
}
@@ -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<PositionedStack> 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;
}
}
@@ -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<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> others) {
if (result != null) {
return allMatch(result);
} else {
// GT recipes use others as a result
return allMatch(others);
}
}
}
@@ -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<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> others) {
return anyMatch(ingredients);
}
}
@@ -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<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> others) {
return anyMatch(ingredients) || anyMatch(result) || anyMatch(others);
}
}
@@ -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<ItemFilter> filters;
public AnyMultiItemFilter(List<ItemFilter> 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;
}
}
@@ -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<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> others) {
return anyMatch(others);
}
}
@@ -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<PositionedStack> 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;
}
}
@@ -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<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> others) {
if (result != null) {
return anyMatch(result);
} else {
// GT recipes use others as a result
return anyMatch(others);
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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<H extends IRecipeHandler> 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<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> others) {
if (matchPositionedStack(ingredients)) {
return true;
}
if (matchPositionedStack(result)) {
return true;
}
if (matchPositionedStack(others)) {
return true;
}
return false;
}
private boolean matchPositionedStack(List<PositionedStack> 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<IRecipeFilter> filters;
public AllMultiRecipeFilter(List<IRecipeFilter> filters) {
this.filters = filters;
}
public AllMultiRecipeFilter(IRecipeFilter filters) {
this(Arrays.asList(filters));
}
public AllMultiRecipeFilter() {
this(new ArrayList<>());
}
@Override
public boolean matches(IRecipeHandler handler, List<PositionedStack> ingredients, PositionedStack result,
List<PositionedStack> 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<H extends IRecipeHandler> 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<Integer> filtered = searchHandler.getSearchResult(filter);
if (filtered == null) {
@@ -546,7 +467,7 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
synchronized (recipeFilterers) {
for (IRecipeFilterProvider p : recipeFilterers) {
IRecipeFilter filter = p.getFilter();
IRecipeFilter filter = p.getRecipeFilter();
if (filter != null) {
recipeFilter.filters.add(filter);
}
@@ -0,0 +1,17 @@
package codechicken.nei.search;
import codechicken.nei.SearchTokenParser;
public abstract class AbstractSearchExpressionVisitor<T> extends SearchExpressionParserBaseVisitor<T> {
protected final SearchTokenParser searchParser;
protected AbstractSearchExpressionVisitor(SearchTokenParser searchParser) {
this.searchParser = searchParser;
}
public SearchTokenParser getSearchParser() {
return searchParser;
}
}
@@ -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<ItemFilter> {
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<List<ItemFilter>, 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<ItemFilter> 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<ItemFilter> filters = searchParser.getAlwaysProvidersFilters(searchText);
return constructFilter(filters, AnyMultiItemFilter::new);
}
private ItemFilter constructFilter(List<ItemFilter> filters, Function<List<ItemFilter>, ItemFilter> constructor) {
if (!filters.isEmpty()) {
// Propagate the result up
if (filters.size() == 1) {
return filters.get(0);
}
return constructor.apply(filters);
} else {
return defaultResult();
}
}
}
@@ -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<IRecipeFilter> {
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<IRecipeFilter> 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<ItemFilter, IRecipeFilter> createAnyFilter, Function<ItemFilter, IRecipeFilter> createAllFilter) {
if (allRecipe) {
return createAllFilter.apply(itemFilter);
} else {
return createAnyFilter.apply(itemFilter);
}
}
private IRecipeFilter constructFilter(List<IRecipeFilter> filters) {
if (!filters.isEmpty()) {
// Propagate the result up
if (filters.size() == 1) {
return filters.get(0);
}
return new AllMultiRecipeFilter(filters);
} else {
return defaultResult();
}
}
}
@@ -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);
}
}
@@ -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<String> {
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;
}
}
@@ -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<Integer, EnumChatFormatting> 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> T visitSearchExpression(String text, AbstractSearchExpressionVisitor<T> visitor) {
SearchExpressionParser parser = createSearchExpressionParser(text, visitor.getSearchParser(), visitor);
return visitor.visitRecipeSearchExpression(parser.recipeSearchExpression());
}
private static <T> SearchExpressionParser createSearchExpressionParser(String text, SearchTokenParser searchParser,
SearchExpressionParserBaseVisitor<T> 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;
}
}
@@ -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;
@@ -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"
@@ -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<Pattern, ItemFilter> 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<NEIClientConfig> 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<Arguments> combinationsProvider(ModeValue[] spaceModes, ModeValue[] patternModes) {
List<Arguments> 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<Arguments> allModesTestProvider = combinationsProvider(spaceModes, patternModes);
private static final List<Arguments> extendedPlusAllSpaceModesTestProvider = combinationsProvider(
spaceModes,
extendedPlusPatternMode);
private static final List<Arguments> spaceIsSpaceAllModesTestProvider = combinationsProvider(
spaceIsSpaceMode,
patternModes);
private static final List<Arguments> spaceIsAndAllModesTestProvider = combinationsProvider(
spaceIsAndMode,
patternModes);
private static final List<Arguments> extendedAllSpaceModesTestProvider = combinationsProvider(
spaceModes,
extendedPatternModes);
private static final List<Arguments> 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<Minecraft> 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<Pattern, ItemFilter> createFilter) {
return new SearchParserProvider(prefix, name, EnumChatFormatting.RESET, createFilter) {
@Override
public SearchTokenParser.SearchMode getSearchMode() {
return searchMode;
}
@Override
public List<Language> getMatchingLanguages() {
List<Language> languages = new ArrayList<>();
languages.add(language);
return languages;
}
};
}
private static String printResult(Set<ItemStack> 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();
}
}