Add PathSearch component

- New PatchSearch component
- Allow user to define base directory for search
- Show (using single select list) search results
- Allow user to define search string
- Implement algorithms(start with exact-match and fuzzy-match) from fuzzy search tool (fzf)
- Sample "component path search" and change "component path" to "component path input"
- Fixes #556
This commit is contained in:
Janne Valkealahti
2022-10-18 15:30:18 +01:00
parent 267c0e6fa4
commit 4bab975ecf
26 changed files with 3605 additions and 5 deletions

View File

@@ -1,6 +1,7 @@
version=3.0.0-SNAPSHOT
springBootVersion=3.0.0-M5
nativeBuildToolsVersion=0.9.14
commonsIoVersion=2.11.0
jlineVersion=3.21.0
st4Version=4.3.1
jimfsVersion=1.2

View File

@@ -12,6 +12,7 @@ dependencies {
api('org.springframework:spring-messaging')
api('org.jline:jline')
api('org.antlr:ST4')
api('commons-io:commons-io')
compileOnly 'com.google.code.findbugs:jsr305'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.awaitility:awaitility'

View File

@@ -0,0 +1,661 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.component;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
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.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.io.file.AccumulatorPathVisitor;
import org.apache.commons.io.file.Counters;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.NotFileFilter;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.jline.keymap.BindingReader;
import org.jline.keymap.KeyMap;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedString;
import org.jline.utils.InfoCmp.Capability;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.shell.component.PathSearch.PathSearchContext;
import org.springframework.shell.component.PathSearch.PathSearchContext.PathViewItem;
import org.springframework.shell.component.context.ComponentContext;
import org.springframework.shell.component.support.AbstractTextComponent;
import org.springframework.shell.component.support.Nameable;
import org.springframework.shell.component.support.SelectorList;
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext;
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext.MessageLevel;
import org.springframework.shell.style.PartsText;
import org.springframework.shell.style.PartsText.PartText;
import org.springframework.shell.support.search.SearchMatch;
import org.springframework.shell.support.search.SearchMatchResult;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import static org.jline.keymap.KeyMap.ctrl;
import static org.jline.keymap.KeyMap.key;
/**
* Component resolving {@link Path} based on base path and optional search term.
* User is expected to type a base path and then delimited by space and a search
* term.
*
* Based on algorithms i.e. from https://github.com/junegunn/fzf and other
* sources.
*
* @author Janne Valkealahti
*/
public class PathSearch extends AbstractTextComponent<Path, PathSearchContext> {
private final static Logger log = LoggerFactory.getLogger(PathSearch.class);
private final static String DEFAULT_TEMPLATE_LOCATION = "classpath:org/springframework/shell/component/path-search-default.stg";
private final PathSearchConfig config;
private PathSearchContext currentContext;
private Function<String, Path> pathProvider = (path) -> Paths.get(path);
private final SelectorList<PathViewItem> selectorList;
public PathSearch(Terminal terminal) {
this(terminal, null);
}
public PathSearch(Terminal terminal, String name) {
this(terminal, name, null);
}
public PathSearch(Terminal terminal, String name, PathSearchConfig config) {
this(terminal, name, config, null);
}
public PathSearch(Terminal terminal, String name, PathSearchConfig config,
Function<PathSearchContext, List<AttributedString>> renderer) {
super(terminal, name, null);
setRenderer(renderer != null ? renderer : new DefaultRenderer());
setTemplateLocation(DEFAULT_TEMPLATE_LOCATION);
this.config = config != null ? config : new PathSearchConfig();
this.selectorList = SelectorList.of(this.config.getMaxPathsShow());
}
@Override
protected void bindKeyMap(KeyMap<String> keyMap) {
super.bindKeyMap(keyMap);
// additional binding what parent gives us
keyMap.bind(OPERATION_DOWN, ctrl('E'), key(getTerminal(), Capability.key_down));
keyMap.bind(OPERATION_UP, ctrl('Y'), key(getTerminal(), Capability.key_up));
}
@Override
public PathSearchContext getThisContext(ComponentContext<?> context) {
if (context != null && currentContext == context) {
return currentContext;
}
currentContext = PathSearchContext.empty();
currentContext.setName(getName());
currentContext.setTerminalWidth(getTerminal().getWidth());
currentContext.setPathSearchConfig(this.config);
currentContext.setMessage("Type '<path> <pattern>' to search", MessageLevel.INFO);
context.stream().forEach(e -> {
currentContext.put(e.getKey(), e.getValue());
});
return currentContext;
}
@Override
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, PathSearchContext context) {
String operation = bindingReader.readBinding(keyMap);
log.debug("Binding read result {}", operation);
if (operation == null) {
return true;
}
String input;
switch (operation) {
case OPERATION_CHAR:
String lastBinding = bindingReader.getLastBinding();
input = context.getInput();
if (input == null) {
input = lastBinding;
}
else {
input = input + lastBinding;
}
context.setInput(input);
inputUpdated(context, input);
break;
case OPERATION_BACKSPACE:
input = context.getInput();
if (StringUtils.hasLength(input)) {
input = input.length() > 1 ? input.substring(0, input.length() - 1) : null;
}
context.setInput(input);
inputUpdated(context, input);
break;
case OPERATION_EXIT:
PathViewItem selected = selectorList.getSelected();
if (selected != null) {
context.setResultValue(selected.getPath());
}
return true;
case OPERATION_UP:
selectorList.scrollUp();
selectorListUpdated(context);
break;
case OPERATION_DOWN:
selectorList.scrollDown();
selectorListUpdated(context);
break;
default:
break;
}
return false;
}
/**
* Sets a path provider.
*
* @param pathProvider the path provider
*/
public void setPathProvider(Function<String, Path> pathProvider) {
this.pathProvider = pathProvider;
}
/**
* Resolves a {@link Path} from a given raw {@code path}.
*
* @param path the raw path
* @return a resolved path
*/
protected Path resolvePath(String path) {
return this.pathProvider.apply(path);
}
private void inputUpdated(PathSearchContext context, String input) {
context.setMessage("Type '<path> <pattern>' to search", MessageLevel.INFO);
updateSelectorList(input, context);
selectorListUpdated(context);
}
private void selectorListUpdated(PathSearchContext context) {
List<PathViewItem> pathViews = selectorList.getProjection().stream()
.map(i -> {
return new PathViewItem(i.getItem().getPath(), i.getItem().getPartsText(), i.isSelected());
})
.collect(Collectors.toList());
context.setPathViewItems(pathViews);
}
private void updateSelectorList(String path, PathSearchContext context) {
if (path == null) {
// when user removes all input
this.selectorList.reset(Collections.emptyList());
return;
}
PathScannerResult result = this.config.pathScanner.get().apply(path, context);
List<PathViewItem> items = result.getScoredPaths().stream()
.filter(scoredPath -> {
if (result.hasFilter()) {
return scoredPath.result.getScore() > 0;
}
else {
return true;
}
})
.map(scoredPath -> {
int[] positions = scoredPath.getResult().getPositions();
String text = scoredPath.getPath().toString();
if (!StringUtils.hasText(text)) {
text = ".";
}
PartsText partsText = PathSearchContext.ofNameMatchPartsx(text, positions);
PathViewItem item = new PathViewItem(scoredPath.getPath(), partsText, false);
return item;
})
.collect(Collectors.toList());
long total = result.getDirCount() + result.getFileCount();
if (total > -1) {
int found = items.size();
String message = String.format(", %s/%s", found, total);
context.setMessage("Type '<path> <pattern>' to search" + message, MessageLevel.INFO);
}
selectorList.reset(items);
}
/**
* Class defining configuration for path search.
*/
public static class PathSearchConfig {
private int maxPathsShow = 5;
private int maxPathsSearch = 20;
private boolean searchForward = true;
private boolean searchCaseSensitive = false;
private boolean searchNormalize = false;
private Supplier<BiFunction<String, PathSearchContext, PathScannerResult>> pathScanner = () -> DefaultPathScanner.of();
public int getMaxPathsShow() {
return this.maxPathsShow;
}
public void setMaxPathsShow(int maxPathsShow) {
Assert.state(maxPathsShow > 0 || maxPathsShow < 33, "maxPathsShow has to be between 1 and 32");
this.maxPathsShow = maxPathsShow;
}
public int getMaxPathsSearch() {
return this.maxPathsSearch;
}
public void setMaxPathsSearch(int maxPathsSearch) {
Assert.state(maxPathsSearch > 0, "maxPathsSearch has to be more than 0");
this.maxPathsSearch = maxPathsSearch;
}
public void setPathScanner(Supplier<BiFunction<String, PathSearchContext, PathScannerResult>> pathScanner) {
Assert.notNull(pathScanner, "pathScanner supplier cannot be null");
this.pathScanner = pathScanner;
}
public boolean isSearchForward() {
return searchForward;
}
public void setSearchForward(boolean searchForward) {
this.searchForward = searchForward;
}
public boolean isSearchCaseSensitive() {
return searchCaseSensitive;
}
public void setSearchCaseSensitive(boolean searchCaseSensitive) {
this.searchCaseSensitive = searchCaseSensitive;
}
public boolean isSearchNormalize() {
return searchNormalize;
}
public void setSearchNormalize(boolean searchNormalize) {
this.searchNormalize = searchNormalize;
}
}
/**
* Result from a path scanning.
*/
public static class PathScannerResult {
private final List<ScoredPath> scoredPaths;
private long dirCount = -1;
private long fileCount = -1;
private boolean hasFilter = false;
PathScannerResult(List<ScoredPath> scoredPaths, long dirCount, long fileCount, boolean hasFilter) {
Assert.notNull(scoredPaths, "Scored paths cannot be null");
this.scoredPaths = scoredPaths;
this.dirCount = dirCount;
this.fileCount = fileCount;
this.hasFilter = hasFilter;
}
public static PathScannerResult of(List<ScoredPath> scoredPaths, boolean hasFilter) {
return new PathScannerResult(scoredPaths, -1, -1, hasFilter);
}
public static PathScannerResult of(List<ScoredPath> scoredPaths, long dirCount, long fileCount, boolean hasFilter) {
return new PathScannerResult(scoredPaths, dirCount, fileCount, hasFilter);
}
public List<ScoredPath> getScoredPaths() {
return scoredPaths;
}
public long getDirCount() {
return dirCount;
}
public long getFileCount() {
return fileCount;
}
public boolean hasFilter() {
return hasFilter;
}
}
/**
* Context for {@link PathSearch}.
*/
public interface PathSearchContext extends TextComponentContext<Path, PathSearchContext> {
/**
* Gets a path view items.
*
* @return path view items
*/
List<PathViewItem> getPathViewItems();
/**
* Sets a path view items.
*
* @param items the path view items
*/
void setPathViewItems(List<PathViewItem> items);
/**
* Get path search config.
*
* @return a path search config
*/
PathSearchConfig getPathSearchConfig();
/**
* Sets a path search config.
*
* @param config a path search config
*/
void setPathSearchConfig(PathSearchConfig config);
/**
* Gets an empty {@link PathSearchContext}.
*
* @return empty path search context
*/
public static PathSearchContext empty() {
return new DefaultPathSearchContext();
}
/**
* Domain class for path view item. Having its index, name(path), ref to cursor
* row index and list of name match parts.
*/
public static class PathViewItem implements Nameable {
private Path path;
private PartsText partsText;
private boolean selected;
public PathViewItem(Path path, PartsText partsText, boolean selected) {
this.path = path;
this.partsText = partsText;
this.selected = selected;
}
@Override
public String getName() {
return path.toString();
}
public Path getPath() {
return path;
}
public boolean isSelected() {
return selected;
}
public PartsText getPartsText() {
return partsText;
}
}
/**
* Split given text into {@link PartText}'s by given positions.
*
* @param text the text to split
* @param positions the positions array, expected to be ordered and no duplicates
* @return parts text
*/
public static PartsText ofNameMatchPartsx(String text, int[] positions) {
List<PartText> parts = new ArrayList<>();
if (positions.length == 0) {
parts.addAll(nameMatchPartsx(text, -1));
}
else if (positions.length == 1 && positions[0] == text.length()) {
parts.addAll(nameMatchPartsx(text, text.length() - 1));
}
else {
int sidx = 0;
int eidx = 0;
for (int i = 0; i < positions.length; i++) {
eidx = positions[i];
if (sidx < text.length()) {
String partText = text.substring(sidx, eidx + 1);
parts.addAll(nameMatchPartsx(partText, eidx - sidx));
}
else {
parts.addAll(nameMatchPartsx(String.valueOf(text.charAt(text.length() - 1)), 0));
}
sidx = eidx + 1;
}
if (sidx < text.length()) {
String partText = text.substring(sidx, text.length());
parts.addAll(nameMatchPartsx(partText, -1));
}
}
return PartsText.of(parts);
}
static List<PartText> nameMatchPartsx(String text, int position) {
List<PartText> parts = new ArrayList<>();
if (position < 0) {
parts.add(PartText.of(text, false));
}
else {
if (position == 0) {
if (text.length() == 1) {
parts.add(PartText.of(String.valueOf(text.charAt(0)), true));
}
else {
parts.add(PartText.of(String.valueOf(text.charAt(0)), true));
parts.add(PartText.of(text.substring(1, text.length()), false));
}
}
else if (position == text.length() - 1) {
parts.add(PartText.of(text.substring(0, text.length() - 1), false));
parts.add(PartText.of(String.valueOf(text.charAt(text.length() - 1)), true));
}
else {
parts.add(PartText.of(text.substring(0, position), false));
parts.add(PartText.of(String.valueOf(text.charAt(position)), true));
parts.add(PartText.of(text.substring(position + 1, text.length()), false));
}
}
return parts;
}
}
private static class DefaultPathSearchContext extends BaseTextComponentContext<Path, PathSearchContext>
implements PathSearchContext {
private List<PathViewItem> pathViewItems;
private PathSearchConfig pathSearchConfig;
@Override
public List<PathViewItem> getPathViewItems() {
return this.pathViewItems;
}
@Override
public void setPathViewItems(List<PathViewItem> pathViewItems) {
this.pathViewItems = pathViewItems;
}
@Override
public PathSearchConfig getPathSearchConfig() {
return this.pathSearchConfig;
}
@Override
public void setPathSearchConfig(PathSearchConfig config) {
this.pathSearchConfig = config;
}
@Override
public Map<String, Object> toTemplateModel() {
Map<String, Object> attributes = super.toTemplateModel();
attributes.put("pathViewItems", getPathViewItems());
Map<String, Object> model = new HashMap<>();
model.put("model", attributes);
return model;
}
}
private class DefaultRenderer implements Function<PathSearchContext, List<AttributedString>> {
@Override
public List<AttributedString> apply(PathSearchContext context) {
return renderTemplateResource(context.toTemplateModel());
}
}
/**
* Holder class keeping {@link Path} and {@link SearchMatchResult}.
*/
public static class ScoredPath implements Comparable<ScoredPath> {
private final Path path;
private final SearchMatchResult result;
ScoredPath(Path path, SearchMatchResult result) {
this.path = path;
this.result = result;
}
public static ScoredPath of(Path path, SearchMatchResult result) {
return new ScoredPath(path, result);
}
public Path getPath() {
return this.path;
}
public SearchMatchResult getResult() {
return this.result;
}
@Override
public int compareTo(ScoredPath other) {
int scoreCompare = Integer.compare(other.result.getScore(), this.result.getScore());
if (scoreCompare == 0) {
// secondary sort by path length
return -Integer.compare(other.getPath().toString().length(), this.getPath().toString().length());
}
return scoreCompare;
}
}
private static class DefaultPathScanner implements BiFunction<String, PathSearchContext, PathScannerResult> {
static DefaultPathScanner of() {
return new DefaultPathScanner();
}
@Override
public PathScannerResult apply(String input, PathSearchContext context) {
// input format <path> <pattern>
String[] split = input.split(" ", 2);
String match = split.length == 2 ? split[1] : null;
// walk files to find candidates
PathSearchPathVisitor visitor = new PathSearchPathVisitor(context.getPathSearchConfig().getMaxPathsSearch());
try {
String p = split[0];
if (".".equals(p)) {
p = "";
}
Path path = Path.of(p);
log.debug("Walking input {} for path {}", input, path);
Files.walkFileTree(path, visitor);
log.debug("walked files {} dirs {}", visitor.getPathCounters().getFileCounter().get(),
visitor.getPathCounters().getDirectoryCounter().get());
} catch (Exception e) {
log.debug("PathSearchPathVisitor caused exception", e);
}
// match and score candidates
Set<ScoredPath> treeSet = new HashSet<ScoredPath>();
Stream.concat(visitor.getFileList().stream(), visitor.getDirList().stream())
.forEach(p -> {
SearchMatchResult result;
if (StringUtils.hasText(match)) {
SearchMatch searchMatch = SearchMatch.builder()
.caseSensitive(context.getPathSearchConfig().isSearchCaseSensitive())
.normalize(context.getPathSearchConfig().isSearchNormalize())
.forward(context.getPathSearchConfig().searchForward)
.build();
result = searchMatch.match(p.toString(), match);
}
else {
result = SearchMatchResult.ofMinus();
}
treeSet.add(ScoredPath.of(p, result));
});
// sort and limit
return treeSet.stream()
.sorted()
.limit(context.getPathSearchConfig().getMaxPathsSearch())
.collect(Collectors.collectingAndThen(Collectors.toList(),
list -> PathScannerResult.of(list, visitor.getPathCounters().getDirectoryCounter().get(),
visitor.getPathCounters().getFileCounter().get(), StringUtils.hasText(match))));
}
}
/**
* Extension to AccumulatorPathVisitor which allows to break out from scanning
* when enough results are found.
*/
private static class PathSearchPathVisitor extends AccumulatorPathVisitor {
private final int limitFiles;
private final static IOFileFilter DNFILTER = new NotFileFilter(new WildcardFileFilter(".*"));
PathSearchPathVisitor(int limitFiles) {
super(Counters.longPathCounters(), DNFILTER, DNFILTER);
this.limitFiles = limitFiles;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
FileVisitResult result = super.visitFile(file, attributes);
if (getPathCounters().getFileCounter().get() >= this.limitFiles) {
return FileVisitResult.TERMINATE;
}
return result;
}
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.component.support;
import java.util.ArrayList;
import java.util.List;
public interface SelectorList<T extends Nameable> {
void reset(List<T> items);
T getSelected();
void scrollUp();
void scrollDown();
List<ProjectionItem<T>> getProjection();
public static <T extends Nameable> SelectorList<T> of(int max) {
return new DefaultSelectorList<T>(max);
}
public interface ProjectionItem<T> extends Nameable, Selectable {
T getItem();
}
static class DefaultSelectorList<T extends Nameable> implements SelectorList<T> {
private final List<T> items = new ArrayList<>();
private final int max;
private int start;
private int position;
public DefaultSelectorList(int max) {
this.max = max;
}
@Override
public void reset(List<T> items) {
this.items.clear();
this.items.addAll(items);
this.start = 0;
this.position = 0;
}
@Override
public T getSelected() {
int index = start + position;
if (this.items.isEmpty()) {
return null;
}
return this.items.get(index);
}
@Override
public void scrollUp() {
// at highest position in page, scroll up
if (start > 0 && position == 0) {
start--;
}
// at highest position, can't go furter, start over from bottom
else if (start + position <= 0) {
if (items.size() < max) {
start = 0;
position = items.size() - 1;
}
else {
start = items.size() - max;
position = max - 1;
}
}
// moving up in same page
else {
position--;
}
}
@Override
public void scrollDown() {
// moving down in same page
if (start + position + 1 < Math.min(items.size(), max)) {
position++;
}
// at lowest position in page, can't go further, start over from top
else if(start + position + 1 >= items.size()) {
start = 0;
position = 0;
}
// in middle of a page, nor highest or lowest
else if(position < max - 1) {
position++;
}
// at lowest position in page, scroll down
else {
start++;
}
}
@Override
public List<ProjectionItem<T>> getProjection() {
List<ProjectionItem<T>> projection = new ArrayList<>();
for (int i = start; i < start + Math.min(items.size(), max); i++) {
boolean selected = i == start + position;
BaseProjectionItem<T> item = new BaseProjectionItem<>(items.get(i), selected);
projection.add(item);
}
return projection;
}
}
static class BaseProjectionItem<T extends Nameable> implements ProjectionItem<T> {
private final T item;
private final boolean selected;
BaseProjectionItem(T item, boolean selected) {
this.item = item;
this.selected = selected;
}
@Override
public T getItem() {
return item;
}
@Override
public String getName() {
return item.getName();
}
@Override
public boolean isSelected() {
return this.selected;
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.style;
import java.util.Arrays;
import java.util.List;
public class PartsText {
private List<PartText> parts;
PartsText(List<PartText> parts) {
this.parts = parts;
}
public static PartsText of(PartText... parts) {
return new PartsText(Arrays.asList(parts));
}
public static PartsText of(List<PartText> parts) {
return new PartsText(parts);
}
public List<PartText> getParts() {
return parts;
}
public static class PartText {
private String text;
private boolean match;
public PartText(String text, boolean match) {
this.text = text;
this.match = match;
}
public static PartText of(String text, boolean match) {
return new PartText(text, match);
}
public String getText() {
return text;
}
public boolean isMatch() {
return match;
}
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.style;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import org.stringtemplate.v4.AttributeRenderer;
import org.springframework.shell.style.PartsText.PartText;
import org.springframework.util.Assert;
public class PartsTextRenderer implements AttributeRenderer<PartsText> {
private final ThemeResolver themeResolver;
public PartsTextRenderer(ThemeResolver themeResolver) {
Assert.notNull(themeResolver, "themeResolver must be set");
this.themeResolver = themeResolver;
}
@Override
public String toString(PartsText value, String formatString, Locale locale) {
StringBuilder buf = new StringBuilder();
Values values = mapValues(formatString);
int len = 0;
int dots = 2;
int prefix = values.prefix;
int width = values.width;
int max = width - prefix;
List<PartText> parts = value.getParts();
for (int i = 0; i < parts.size(); i++) {
PartText pt = parts.get(i);
String text;
boolean doBreak = false;
int newLen = len + pt.getText().length();
// if current would take over max length
if (newLen > max) {
int l = max - len - dots;
text = String.format(locale, "%1." + l + "s.." , pt.getText());
doBreak = true;
}
// if next would take over max length
else if (i + 1 < parts.size() && newLen + parts.get(i + 1).getText().length() > max) {
int l = max - len - dots;
text = String.format(locale, "%1." + l + "s.." , pt.getText());
doBreak = true;
}
// we're fine as is
else {
text = pt.getText();
}
String tag = pt.isMatch() ? values.matchStyle : values.textStyle;
buf.append(String.format("@{%s %s}", themeResolver.resolveStyleTag(tag), text));
len += pt.getText().length();
if (doBreak) {
break;
}
}
return buf.toString();
}
private static class Values {
Integer width;
Integer prefix;
String textStyle;
String matchStyle;
public void setWidth(Integer width) {
this.width = width;
}
public void setPrefix(Integer prefix) {
this.prefix = prefix;
}
public void setTextStyle(String textStyle) {
this.textStyle = textStyle;
}
public void setMatchStyle(String matchStyle) {
this.matchStyle = matchStyle;
}
}
private static Values mapValues(String expression) {
Values values = new Values();
Stream.of(expression.split(","))
.map(String::trim)
.forEach(v -> {
String[] split = v.split(":", 2);
if (split.length == 2) {
if ("width".equals(split[0])) {
values.setWidth(Integer.parseInt(split[1]));
}
else if ("prefix".equals(split[0])) {
values.setPrefix(Integer.parseInt(split[1]));
}
else if ("textStyle".equals(split[0])) {
values.setTextStyle(split[1]);
}
else if ("matchStyle".equals(split[0])) {
values.setMatchStyle(split[1]);
}
}
});
return values;
}
}

View File

@@ -38,11 +38,13 @@ public class TemplateExecutor {
private final static Logger log = LoggerFactory.getLogger(TemplateExecutor.class);
private final static STErrorListener ERROR_LISTENER = new LoggingSTErrorListener();
private final ThemeResolver themeResolver;
private StringToStyleExpressionRenderer renderer;
private StringToStyleExpressionRenderer renderer1;
private PartsTextRenderer renderer2;
public TemplateExecutor(ThemeResolver themeResolver) {
this.themeResolver = themeResolver;
renderer = new StringToStyleExpressionRenderer(themeResolver);
renderer1 = new StringToStyleExpressionRenderer(themeResolver);
renderer2 = new PartsTextRenderer(themeResolver);
}
/**
@@ -55,7 +57,8 @@ public class TemplateExecutor {
public AttributedString render(String template, Map<String, Object> attributes) {
STGroup group = new STGroup();
group.setListener(ERROR_LISTENER);
group.registerRenderer(String.class, renderer);
group.registerRenderer(String.class, renderer1);
group.registerRenderer(PartsText.class, renderer2);
ST st = new ST(group, template);
if (attributes != null) {
@@ -76,7 +79,8 @@ public class TemplateExecutor {
public AttributedString renderGroup(String template, Map<String, Object> attributes) {
STGroup group = new STGroupString(template);
group.setListener(ERROR_LISTENER);
group.registerRenderer(String.class, renderer);
group.registerRenderer(String.class, renderer1);
group.registerRenderer(PartsText.class, renderer2);
// define styled figures as dictionary
Map<String, Object> figureDict = Stream.of(FigureSettings.tags())

View File

@@ -0,0 +1,278 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.springframework.util.StringUtils;
/**
* Base class for common search algorithms mostly based on {@code fzf}
* algorithms.
*
* @author Janne Valkealahti
*/
abstract class AbstractSearchMatchAlgorithm implements SearchMatchAlgorithm {
// points given to matches
public static final int SCORE_MATCH = 16;
public static final int SCORE_GAP_START = -3;
public static final int SCORE_GAP_EXTENSION = -1;
public static final int BONUS_BOUNDARY = SCORE_MATCH / 2;
public static final int BONUS_NON_WORD = SCORE_MATCH / 2;
public static final int BONUS_CAMEL123 = BONUS_BOUNDARY + SCORE_GAP_EXTENSION;
public static final int BONUS_CONSECUTIVE = -(SCORE_GAP_START + SCORE_GAP_EXTENSION);
public static final int BONUS_FIRST_CHAR_MULTIPLIER = 2;
public static final int BONUS_BOUNDARY_WHITE = BONUS_BOUNDARY + 2; // default +2
public static final int BONUS_BOUNDARY_DELIMITER = BONUS_BOUNDARY + 1 + 1; // default +1
/**
* Enumeration of matched characters.
*/
static enum CharClass {
WHITE,
NONWORD,
DELIMITER,
LOWER,
UPPER,
LETTER,
NUMBER
}
static class CalculateScore {
int score;
int pos[];
CalculateScore(int score, int[] pos) {
this.score = score;
this.pos = pos;
}
}
static int indexAt(int index, int max, boolean forward) {
if (forward) {
return index;
}
return max - index - 1;
}
private final static String DELIMITER_CHARS = "/,:;|";
static CharClass charClassOfAscii(char c) {
if (c >= 'a' && c <= 'z') {
return CharClass.LOWER;
}
else if (c >= 'A' && c <= 'Z') {
return CharClass.UPPER;
}
else if (c >= '0' && c <= '9') {
return CharClass.NUMBER;
}
else if (Character.isWhitespace(c)) {
return CharClass.WHITE;
}
else if (DELIMITER_CHARS.indexOf(c) >= 0) {
return CharClass.DELIMITER;
}
return CharClass.NONWORD;
}
static CharClass charClassOfNonAscii(char c) {
if (Character.isLowerCase(c)) {
return CharClass.LOWER;
}
else if (Character.isUpperCase(c)) {
return CharClass.UPPER;
}
else if (Character.isDigit(c)) {
return CharClass.NUMBER;
}
else if (Character.isLetter(c)) {
return CharClass.LETTER;
}
else if (Character.isWhitespace(c)) {
return CharClass.WHITE;
}
else if (DELIMITER_CHARS.indexOf(c) >= 0) {
return CharClass.DELIMITER;
}
return CharClass.NONWORD;
}
static int bonusFor(CharClass prevClass, CharClass clazz) {
if (clazz.ordinal() > CharClass.NONWORD.ordinal()) {
if (prevClass == CharClass.WHITE) {
return BONUS_BOUNDARY_WHITE;
}
else if (prevClass == CharClass.DELIMITER) {
return BONUS_BOUNDARY_DELIMITER;
}
else if (prevClass == CharClass.NONWORD) {
return BONUS_BOUNDARY;
}
}
if (prevClass == CharClass.LOWER && clazz == CharClass.UPPER ||
prevClass != CharClass.NUMBER && clazz == CharClass.NUMBER) {
return BONUS_CAMEL123;
}
else if (clazz == CharClass.NONWORD) {
return BONUS_NON_WORD;
}
else if (clazz == CharClass.WHITE) {
return BONUS_BOUNDARY_WHITE;
}
return 0;
}
static int bonusAt(String input, int idx) {
if (idx == 0) {
return BONUS_BOUNDARY_WHITE;
}
return bonusFor(charClassOfAscii(input.charAt(idx - 1)), charClassOfAscii(input.charAt(idx)));
}
static CalculateScore calculateScore(boolean caseSensitive, boolean normalize, String text, String pattern,
int sidx, int eidx) {
int pidx = 0;
int score = 0;
boolean inGap = false;
int consecutive = 0;
int firstBonus = 0;
List<Integer> positions = new ArrayList<>();
CharClass prevClass = CharClass.WHITE;
if (sidx > 0) {
prevClass = charClassOfAscii(text.charAt(sidx - 1));
}
for (int idx = sidx; idx < eidx; idx++) {
char c = text.charAt(idx);
CharClass clazz = charClassOfAscii(c);
if (!caseSensitive) {
if (c >= 'A' && c <= 'Z') {
c += 32;
}
}
if (normalize) {
c = normalizeRune(c);
}
if (c == pattern.charAt(pidx)) {
positions.add(idx);
score += SCORE_MATCH;
int bonus = bonusFor(prevClass, clazz);
if (consecutive == 0) {
firstBonus = bonus;
}
else {
if (bonus >= BONUS_BOUNDARY && bonus > firstBonus) {
firstBonus = bonus;
}
bonus = Math.max(Math.max(bonus, firstBonus), BONUS_CONSECUTIVE);
}
if (pidx == 0) {
score += bonus * BONUS_FIRST_CHAR_MULTIPLIER;
}
else {
score += bonus;
}
inGap = false;
consecutive++;
pidx++;
}
else {
if (inGap) {
score += SCORE_GAP_EXTENSION;
}
else {
score += SCORE_GAP_START;
}
inGap = true;
consecutive = 0;
firstBonus = 0;
}
prevClass = clazz;
}
return new CalculateScore(score, positions.stream().mapToInt(Integer::intValue).toArray());
}
static int trySkip(String input, boolean caseSensitive, char b, int from) {
String byteArray = input.substring(from);
int idx = byteArray.indexOf(b);
if (idx == 0) {
return from;
}
if (!caseSensitive && b >= 'a' && b <= 'z') {
if (idx > 0) {
byteArray = byteArray.substring(idx);
}
int uidx = byteArray.indexOf(b - 32);
if (uidx >= 0) {
idx = uidx;
}
}
if (idx < 0) {
return -1;
}
return from + idx;
}
static int asciiFuzzyIndex(String input, String pattern, boolean caseSensitive) {
if (!StringUtils.hasText(input)) {
return 0;
}
if (!Charset.forName("US-ASCII").newEncoder().canEncode(input)) {
return 0;
}
int firstIndex = 0;
int idx = 0;
for (int pidx = 0; pidx < pattern.length(); pidx++) {
idx = trySkip(input, caseSensitive, pattern.charAt(pidx), idx);
if (idx < 0) {
return -1;
}
if (pidx == 0 && idx > 0) {
firstIndex = idx - 1;
}
idx++;
}
return firstIndex;
}
static char normalizeRune(char r) {
if (r < 0x00C0 || r > 0x2184) {
return r;
}
Character n = Normalize.normalized.get(r);
if (n != null && n > 0) {
return n;
}
return r;
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
import org.springframework.util.StringUtils;
/**
* Port of {@code fzf} {@code ExactMatchNaive} algorithm.
*
* @author Janne Valkealahti
*/
class ExactMatchNaiveSearchMatchAlgorithm extends AbstractSearchMatchAlgorithm {
@Override
public SearchMatchResult match(boolean caseSensitive, boolean normalize, boolean forward, String text,
String pattern) {
if (!StringUtils.hasText(pattern)) {
return SearchMatchResult.ofZeros();
}
int lenRunes = text.length();
int lenPattern = pattern.length();
if (lenRunes < lenPattern) {
return SearchMatchResult.ofMinus();
}
int pidx = 0;
int bestPos = -1;
int bonus = 0;
int bestBonus = -1;
for (int index = 0; index < lenRunes; index++) {
int index_ = indexAt(index, lenRunes, forward);
char c = text.charAt(index_);
if (!caseSensitive) {
if (c >= 'A' && c <= 'Z') {
c += 32;
}
if (normalize) {
c = normalizeRune(c);
}
}
int pidx_ = indexAt(pidx, lenPattern, forward);
char pchar = pattern.charAt(pidx_);
if (c == pchar) {
if (pidx_ == 0) {
bonus = bonusAt(text, index_);
}
pidx++;
if (pidx == lenPattern) {
if (bonus > bestBonus) {
bestPos = index;
bestBonus = bonus;
}
if (bonus >= BONUS_BOUNDARY) {
break;
}
index -= pidx - 1;
pidx = 0;
bonus = 0;
}
}
else {
index -= pidx;
pidx = 0;
bonus = 0;
}
}
if (bestPos >= 0) {
int sidx;
int eidx;
if (forward) {
sidx = bestPos - lenPattern + 1;
eidx = bestPos + 1;
}
else {
sidx = lenRunes - (bestPos + 1);
eidx = lenRunes - (bestPos - lenPattern + 1);
}
CalculateScore score = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx);
return SearchMatchResult.of(sidx, eidx, score.score, score.pos, this);
}
return SearchMatchResult.ofMinus();
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
/**
* Port of {@code fzf} {@code FuzzyMatchV1} algorithm.
*
* @author Janne Valkealahti
*/
class FuzzyMatchV1SearchMatchAlgorithm extends AbstractSearchMatchAlgorithm {
@Override
public SearchMatchResult match(boolean caseSensitive, boolean normalize, boolean forward, String text, String pattern) {
int pidx = 0;
int sidx = -1;
int eidx = -1;
int lenRunes = text.length();
int lenPattern = pattern.length();
for (int index = 0; index < lenRunes; index++) {
char c = text.charAt(indexAt(index, lenRunes, forward));
if (!caseSensitive) {
if (c >= 'A' && c <= 'Z') {
c += 32;
}
}
if (normalize) {
c = normalizeRune(c);
}
char pchar = pattern.charAt(indexAt(pidx, lenPattern, forward));
if (c == pchar) {
if (sidx < 0) {
sidx = index;
}
pidx++;
if (pidx == lenPattern) {
eidx = index + 1;
break;
}
}
}
if (sidx >= 0 && eidx >= 0) {
pidx--;
for (int i = eidx - 1; i >= sidx; i--) {
int tidx = indexAt(i, lenRunes, forward);
char c = text.charAt(tidx);
if (!caseSensitive) {
if (c >= 'A' && c <= 'Z') {
c += 32;
}
}
int pidx_ = indexAt(pidx, lenPattern, forward);
char pchar = pattern.charAt(pidx_);
if (c == pchar) {
pidx--;
if (pidx < 0) {
sidx = i;
break;
}
}
}
if (!forward) {
int sidxOrig = sidx;
sidx = lenRunes - eidx;
eidx = lenRunes - sidxOrig;
}
CalculateScore calculateScore = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx);
return SearchMatchResult.of(sidx, eidx, calculateScore.score, calculateScore.pos, this);
}
return SearchMatchResult.of(-1, -1, 0, new int[0], this);
}
}

View File

@@ -0,0 +1,265 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
import java.util.ArrayList;
import java.util.List;
import org.springframework.util.StringUtils;
/**
* Port of {@code fzf} {@code FuzzyMatchV2} algorithm.
*
* @author Janne Valkealahti
*/
class FuzzyMatchV2SearchMatchAlgorithm extends AbstractSearchMatchAlgorithm {
@Override
public SearchMatchResult match(boolean caseSensitive, boolean normalize, boolean forward, String text,
String pattern) {
if (!StringUtils.hasText(pattern)) {
return SearchMatchResult.ofZeros();
}
int M = pattern.length();
int N = text.length();
int idx = asciiFuzzyIndex(text, pattern, caseSensitive);
if (idx < 0) {
return SearchMatchResult.ofMinus();
}
List<Integer> H0 = create(N);
List<Integer> C0 = create(N);
List<Integer> B = create(N);
List<Integer> F = create(M);
String T = text;
// Phase 2. Calculate bonus for each point
int maxScore = 0;
int maxScorePos = 0;
int pidx = 0;
int lastIdx = 0;
char pchar0 = pattern.charAt(0);
char pchar = pattern.charAt(0);
int prevH0 = 0;
CharClass prevClass = CharClass.WHITE;
boolean inGap = false;
int TsubIdxRestore = idx;
String Tsub = T.substring(idx);
List<Integer> H0sub = slicex(H0, idx, Tsub.length());
List<Integer> C0sub = slicex(C0, idx, Tsub.length());
List<Integer> Bsub = slicex(B, idx, Tsub.length());
for (int off = 0; off < Tsub.length(); off++) {
char c = Tsub.charAt(off);
CharClass clazz;
if (c >= 32 && c < 127) {
clazz = charClassOfAscii(c);
if (!caseSensitive && clazz == CharClass.UPPER) {
c += 32;
}
}
else {
clazz = charClassOfNonAscii(c);
if (!caseSensitive && clazz == CharClass.UPPER) {
c = Character.toLowerCase(c);
}
if (normalize) {
c = normalizeRune(c);
}
}
// TODO: potential speed increase as go can directly modify underlying array/slice
// by access via for loop variables and so on. we create a lot of garbage here.
Tsub = Tsub.substring(0, off) + c + Tsub.substring(off + 1);
int bonus = bonusFor(prevClass, clazz);
Bsub.set(off, bonus);
prevClass = clazz;
if (c == pchar) {
if (pidx < M) {
F.set(pidx, idx + off);
pidx++;
pchar = pattern.charAt(Math.min(pidx, M - 1));
}
lastIdx = idx + off;
}
if (c == pchar0) {
int score = SCORE_MATCH + bonus * BONUS_FIRST_CHAR_MULTIPLIER;
H0sub.set(off, score);
C0sub.set(off, 1);
if (M == 1 && (forward && score > maxScore || !forward && score >= maxScore)) {
maxScore = score;
maxScorePos = idx + off;
if (forward && bonus >= BONUS_BOUNDARY) {
break;
}
}
inGap = false;
}
else {
if (inGap) {
H0sub.set(off, Math.max(prevH0 + SCORE_GAP_EXTENSION, 0));
}
else {
H0sub.set(off, Math.max(prevH0 + SCORE_GAP_START, 0));
}
C0sub.set(off, 0);
inGap = true;
}
prevH0 = H0sub.get(off);
}
T = T.substring(0, TsubIdxRestore) + Tsub;
if (pidx != M) {
return SearchMatchResult.ofMinus();
}
if (M == 1) {
return SearchMatchResult.of(maxScorePos, maxScorePos + 1, maxScore, new int[] { maxScorePos }, this);
}
// Phase 3. Fill in score matrix (H)
int f0 = F.get(0);
int width = lastIdx - f0 + 1;
List<Integer> H = create(width * M);
copy(H, H0, f0, lastIdx + 1);
List<Integer> C = create(width * M);
copy(C, C0, f0, lastIdx + 1);
List<Integer> Fsub = F.subList(1, F.size());
String Psub = pattern.substring(1);
Psub = Psub.substring(0, Fsub.size());
for (int off = 0; off < Fsub.size(); off++) {
int f = Fsub.get(off);
char pchar2 = Psub.charAt(off);
int pidx2 = off + 1;
int row = pidx2 * width;
boolean inGap2 = false;
String Tsub2 = T.substring(f, lastIdx + 1);
List<Integer> Bsub2 = slicex(B, f, Tsub2.length());
List<Integer> Csub2 = slicex(C, row + f - f0, Tsub2.length());
List<Integer> Cdiag = slicex(C, row + f - f0 - 1 - width, Tsub2.length());
List<Integer> Hsub2 = slicex(H, row + f - f0, Tsub2.length());
List<Integer> Hdiag = slicex(H, row + f - f0 - 1 - width, Tsub2.length());
List<Integer> Hleft = slicex(H, row + f - f0 - 1, Tsub2.length());
Hleft.set(0, 0);
for (int off2 = 0; off2 < Tsub2.length(); off2++) {
char c = Tsub2.charAt(off2);
int col = off2 + f;
int s1 = 0;
int s2 = 0;
int consecutive = 0;
if (inGap2) {
s2 = Hleft.get(off2) + SCORE_GAP_EXTENSION;
}
else {
s2 = Hleft.get(off2) + SCORE_GAP_START;
}
if (pchar2 == c) {
s1 = Hdiag.get(off2) + SCORE_MATCH;
int b = Bsub2.get(off2);
consecutive = Cdiag.get(off2) + 1;
if (consecutive > 1) {
int fb = B.get(col - consecutive + 1);
if (b >= BONUS_BOUNDARY && b > fb) {
consecutive = 1;
}
else {
b = Math.max(b, Math.max(BONUS_CONSECUTIVE, fb));
}
}
if (s1 + b < s2) {
s1 += Bsub2.get(off2);
consecutive = 0;
}
else {
s1 += b;
}
}
Csub2.set(off2, consecutive);
inGap2 = s1 < s2;
int score = Math.max(Math.max(s1, s2), 0);
if (pidx2 == M - 1 && (forward && score > maxScore) || !forward && score >= maxScore) {
maxScore = score;
maxScorePos = col;
}
Hsub2.set(off2, score);
}
}
// Phase 4. (Optional) Backtrace to find character positions
int[] pos = new int[M];
int j = f0;
int i = M - 1;
j = maxScorePos;
boolean preferMatch = true;
int posidx = pos.length - 1;
for(;;) {
int I = i * width;
int j0 = j - f0;
int s = H.get(I + j0);
int s1 = 0;
int s2 = 0;
if (i > 0 && j >= F.get(i)) {
s1 = H.get(I - width + j0 - 1);
}
if (j > F.get(i)) {
s2 = H.get(I + j0 - 1);
}
if (s > s1 && (s > s2 || s == s2 && preferMatch)) {
pos[posidx--] = j;
if (i == 0) {
break;
}
i--;
}
preferMatch = C.get(I + j0) > 1 || I + width + j0 + 1 < C.size() && C.get(I + width + j0 + 1) > 0;
j--;
}
return SearchMatchResult.of(j, maxScorePos + 1, maxScore, pos, this);
}
private static List<Integer> slicex(List<Integer> from, int start, int length) {
return from.subList(start, from.size()).subList(0, length);
}
private static void copy(List<Integer> dst, List<Integer> src, int start, int end) {
int x = 0;
for (int i = start; i < end; i++) {
dst.set(x++, src.get(i));
}
}
private static List<Integer> create(int size) {
List<Integer> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add(0);
}
return list;
}
}

View File

@@ -0,0 +1,503 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
import java.util.HashMap;
import java.util.Map;
public class Normalize {
public static Map<Character, Character> normalized = new HashMap<>();
static {
normalized.put((char)0x00E1, 'a'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x0103, 'a'); // WITH BREVE, LATIN SMALL LETTER
normalized.put((char)0x01CE, 'a'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x00E2, 'a'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x00E4, 'a'); // WITH DIAERESIS, LATIN SMALL LETTER
normalized.put((char)0x0227, 'a'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1EA1, 'a'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0201, 'a'); // WITH DOUBLE GRAVE, LATIN SMALL LETTER
normalized.put((char)0x00E0, 'a'); // WITH GRAVE, LATIN SMALL LETTER
normalized.put((char)0x1EA3, 'a'); // WITH HOOK ABOVE, LATIN SMALL LETTER
normalized.put((char)0x0203, 'a'); // WITH INVERTED BREVE, LATIN SMALL LETTER
normalized.put((char)0x0101, 'a'); // WITH MACRON, LATIN SMALL LETTER
normalized.put((char)0x0105, 'a'); // WITH OGONEK, LATIN SMALL LETTER
normalized.put((char)0x1E9A, 'a'); // WITH RIGHT HALF RING, LATIN SMALL LETTER
normalized.put((char)0x00E5, 'a'); // WITH RING ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E01, 'a'); // WITH RING BELOW, LATIN SMALL LETTER
normalized.put((char)0x00E3, 'a'); // WITH TILDE, LATIN SMALL LETTER
normalized.put((char)0x0363, 'a'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x0250, 'a'); // , LATIN SMALL LETTER TURNED
normalized.put((char)0x1E03, 'b'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E05, 'b'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0253, 'b'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x1E07, 'b'); // WITH LINE BELOW, LATIN SMALL LETTER
normalized.put((char)0x0180, 'b'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x0183, 'b'); // WITH TOPBAR, LATIN SMALL LETTER
normalized.put((char)0x0107, 'c'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x010D, 'c'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x00E7, 'c'); // WITH CEDILLA, LATIN SMALL LETTER
normalized.put((char)0x0109, 'c'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x0255, 'c'); // WITH CURL, LATIN SMALL LETTER
normalized.put((char)0x010B, 'c'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x0188, 'c'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x023C, 'c'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x0368, 'c'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x0297, 'c'); // , LATIN LETTER STRETCHED
normalized.put((char)0x2184, 'c'); // , LATIN SMALL LETTER REVERSED
normalized.put((char)0x010F, 'd'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x1E11, 'd'); // WITH CEDILLA, LATIN SMALL LETTER
normalized.put((char)0x1E13, 'd'); // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
normalized.put((char)0x0221, 'd'); // WITH CURL, LATIN SMALL LETTER
normalized.put((char)0x1E0B, 'd'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E0D, 'd'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0257, 'd'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x1E0F, 'd'); // WITH LINE BELOW, LATIN SMALL LETTER
normalized.put((char)0x0111, 'd'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x0256, 'd'); // WITH TAIL, LATIN SMALL LETTER
normalized.put((char)0x018C, 'd'); // WITH TOPBAR, LATIN SMALL LETTER
normalized.put((char)0x0369, 'd'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x00E9, 'e'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x0115, 'e'); // WITH BREVE, LATIN SMALL LETTER
normalized.put((char)0x011B, 'e'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x0229, 'e'); // WITH CEDILLA, LATIN SMALL LETTER
normalized.put((char)0x1E19, 'e'); // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
normalized.put((char)0x00EA, 'e'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x00EB, 'e'); // WITH DIAERESIS, LATIN SMALL LETTER
normalized.put((char)0x0117, 'e'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1EB9, 'e'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0205, 'e'); // WITH DOUBLE GRAVE, LATIN SMALL LETTER
normalized.put((char)0x00E8, 'e'); // WITH GRAVE, LATIN SMALL LETTER
normalized.put((char)0x1EBB, 'e'); // WITH HOOK ABOVE, LATIN SMALL LETTER
normalized.put((char)0x025D, 'e'); // WITH HOOK, LATIN SMALL LETTER REVERSED OPEN
normalized.put((char)0x0207, 'e'); // WITH INVERTED BREVE, LATIN SMALL LETTER
normalized.put((char)0x0113, 'e'); // WITH MACRON, LATIN SMALL LETTER
normalized.put((char)0x0119, 'e'); // WITH OGONEK, LATIN SMALL LETTER
normalized.put((char)0x0247, 'e'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x1E1B, 'e'); // WITH TILDE BELOW, LATIN SMALL LETTER
normalized.put((char)0x1EBD, 'e'); // WITH TILDE, LATIN SMALL LETTER
normalized.put((char)0x0364, 'e'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x029A, 'e'); // , LATIN SMALL LETTER CLOSED OPEN
normalized.put((char)0x025E, 'e'); // , LATIN SMALL LETTER CLOSED REVERSED OPEN
normalized.put((char)0x025B, 'e'); // , LATIN SMALL LETTER OPEN
normalized.put((char)0x0258, 'e'); // , LATIN SMALL LETTER REVERSED
normalized.put((char)0x025C, 'e'); // , LATIN SMALL LETTER REVERSED OPEN
normalized.put((char)0x01DD, 'e'); // , LATIN SMALL LETTER TURNED
normalized.put((char)0x1D08, 'e'); // , LATIN SMALL LETTER TURNED OPEN
normalized.put((char)0x1E1F, 'f'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x0192, 'f'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x01F5, 'g'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x011F, 'g'); // WITH BREVE, LATIN SMALL LETTER
normalized.put((char)0x01E7, 'g'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x0123, 'g'); // WITH CEDILLA, LATIN SMALL LETTER
normalized.put((char)0x011D, 'g'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x0121, 'g'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x0260, 'g'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x1E21, 'g'); // WITH MACRON, LATIN SMALL LETTER
normalized.put((char)0x01E5, 'g'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x0261, 'g'); // , LATIN SMALL LETTER SCRIPT
normalized.put((char)0x1E2B, 'h'); // WITH BREVE BELOW, LATIN SMALL LETTER
normalized.put((char)0x021F, 'h'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x1E29, 'h'); // WITH CEDILLA, LATIN SMALL LETTER
normalized.put((char)0x0125, 'h'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x1E27, 'h'); // WITH DIAERESIS, LATIN SMALL LETTER
normalized.put((char)0x1E23, 'h'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E25, 'h'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x02AE, 'h'); // WITH FISHHOOK, LATIN SMALL LETTER TURNED
normalized.put((char)0x0266, 'h'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x1E96, 'h'); // WITH LINE BELOW, LATIN SMALL LETTER
normalized.put((char)0x0127, 'h'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x036A, 'h'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x0265, 'h'); // , LATIN SMALL LETTER TURNED
normalized.put((char)0x2095, 'h'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x00ED, 'i'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x012D, 'i'); // WITH BREVE, LATIN SMALL LETTER
normalized.put((char)0x01D0, 'i'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x00EE, 'i'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x00EF, 'i'); // WITH DIAERESIS, LATIN SMALL LETTER
normalized.put((char)0x1ECB, 'i'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0209, 'i'); // WITH DOUBLE GRAVE, LATIN SMALL LETTER
normalized.put((char)0x00EC, 'i'); // WITH GRAVE, LATIN SMALL LETTER
normalized.put((char)0x1EC9, 'i'); // WITH HOOK ABOVE, LATIN SMALL LETTER
normalized.put((char)0x020B, 'i'); // WITH INVERTED BREVE, LATIN SMALL LETTER
normalized.put((char)0x012B, 'i'); // WITH MACRON, LATIN SMALL LETTER
normalized.put((char)0x012F, 'i'); // WITH OGONEK, LATIN SMALL LETTER
normalized.put((char)0x0268, 'i'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x1E2D, 'i'); // WITH TILDE BELOW, LATIN SMALL LETTER
normalized.put((char)0x0129, 'i'); // WITH TILDE, LATIN SMALL LETTER
normalized.put((char)0x0365, 'i'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x0131, 'i'); // , LATIN SMALL LETTER DOTLESS
normalized.put((char)0x1D09, 'i'); // , LATIN SMALL LETTER TURNED
normalized.put((char)0x1D62, 'i'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x2071, 'i'); // , SUPERSCRIPT LATIN SMALL LETTER
normalized.put((char)0x01F0, 'j'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x0135, 'j'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x029D, 'j'); // WITH CROSSED-TAIL, LATIN SMALL LETTER
normalized.put((char)0x0249, 'j'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x025F, 'j'); // WITH STROKE, LATIN SMALL LETTER DOTLESS
normalized.put((char)0x0237, 'j'); // , LATIN SMALL LETTER DOTLESS
normalized.put((char)0x1E31, 'k'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x01E9, 'k'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x0137, 'k'); // WITH CEDILLA, LATIN SMALL LETTER
normalized.put((char)0x1E33, 'k'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0199, 'k'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x1E35, 'k'); // WITH LINE BELOW, LATIN SMALL LETTER
normalized.put((char)0x029E, 'k'); // , LATIN SMALL LETTER TURNED
normalized.put((char)0x2096, 'k'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x013A, 'l'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x019A, 'l'); // WITH BAR, LATIN SMALL LETTER
normalized.put((char)0x026C, 'l'); // WITH BELT, LATIN SMALL LETTER
normalized.put((char)0x013E, 'l'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x013C, 'l'); // WITH CEDILLA, LATIN SMALL LETTER
normalized.put((char)0x1E3D, 'l'); // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
normalized.put((char)0x0234, 'l'); // WITH CURL, LATIN SMALL LETTER
normalized.put((char)0x1E37, 'l'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x1E3B, 'l'); // WITH LINE BELOW, LATIN SMALL LETTER
normalized.put((char)0x0140, 'l'); // WITH MIDDLE DOT, LATIN SMALL LETTER
normalized.put((char)0x026B, 'l'); // WITH MIDDLE TILDE, LATIN SMALL LETTER
normalized.put((char)0x026D, 'l'); // WITH RETROFLEX HOOK, LATIN SMALL LETTER
normalized.put((char)0x0142, 'l'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x2097, 'l'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x1E3F, 'm'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x1E41, 'm'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E43, 'm'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0271, 'm'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x0270, 'm'); // WITH LONG LEG, LATIN SMALL LETTER TURNED
normalized.put((char)0x036B, 'm'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x1D1F, 'm'); // , LATIN SMALL LETTER SIDEWAYS TURNED
normalized.put((char)0x026F, 'm'); // , LATIN SMALL LETTER TURNED
normalized.put((char)0x2098, 'm'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x0144, 'n'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x0148, 'n'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x0146, 'n'); // WITH CEDILLA, LATIN SMALL LETTER
normalized.put((char)0x1E4B, 'n'); // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
normalized.put((char)0x0235, 'n'); // WITH CURL, LATIN SMALL LETTER
normalized.put((char)0x1E45, 'n'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E47, 'n'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x01F9, 'n'); // WITH GRAVE, LATIN SMALL LETTER
normalized.put((char)0x0272, 'n'); // WITH LEFT HOOK, LATIN SMALL LETTER
normalized.put((char)0x1E49, 'n'); // WITH LINE BELOW, LATIN SMALL LETTER
normalized.put((char)0x019E, 'n'); // WITH LONG RIGHT LEG, LATIN SMALL LETTER
normalized.put((char)0x0273, 'n'); // WITH RETROFLEX HOOK, LATIN SMALL LETTER
normalized.put((char)0x00F1, 'n'); // WITH TILDE, LATIN SMALL LETTER
normalized.put((char)0x2099, 'n'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x00F3, 'o'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x014F, 'o'); // WITH BREVE, LATIN SMALL LETTER
normalized.put((char)0x01D2, 'o'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x00F4, 'o'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x00F6, 'o'); // WITH DIAERESIS, LATIN SMALL LETTER
normalized.put((char)0x022F, 'o'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1ECD, 'o'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0151, 'o'); // WITH DOUBLE ACUTE, LATIN SMALL LETTER
normalized.put((char)0x020D, 'o'); // WITH DOUBLE GRAVE, LATIN SMALL LETTER
normalized.put((char)0x00F2, 'o'); // WITH GRAVE, LATIN SMALL LETTER
normalized.put((char)0x1ECF, 'o'); // WITH HOOK ABOVE, LATIN SMALL LETTER
normalized.put((char)0x01A1, 'o'); // WITH HORN, LATIN SMALL LETTER
normalized.put((char)0x020F, 'o'); // WITH INVERTED BREVE, LATIN SMALL LETTER
normalized.put((char)0x014D, 'o'); // WITH MACRON, LATIN SMALL LETTER
normalized.put((char)0x01EB, 'o'); // WITH OGONEK, LATIN SMALL LETTER
normalized.put((char)0x00F8, 'o'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x1D13, 'o'); // WITH STROKE, LATIN SMALL LETTER SIDEWAYS
normalized.put((char)0x00F5, 'o'); // WITH TILDE, LATIN SMALL LETTER
normalized.put((char)0x0366, 'o'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x0275, 'o'); // , LATIN SMALL LETTER BARRED
normalized.put((char)0x1D17, 'o'); // , LATIN SMALL LETTER BOTTOM HALF
normalized.put((char)0x0254, 'o'); // , LATIN SMALL LETTER OPEN
normalized.put((char)0x1D11, 'o'); // , LATIN SMALL LETTER SIDEWAYS
normalized.put((char)0x1D12, 'o'); // , LATIN SMALL LETTER SIDEWAYS OPEN
normalized.put((char)0x1D16, 'o'); // , LATIN SMALL LETTER TOP HALF
normalized.put((char)0x1E55, 'p'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x1E57, 'p'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x01A5, 'p'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x209A, 'p'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x024B, 'q'); // WITH HOOK TAIL, LATIN SMALL LETTER
normalized.put((char)0x02A0, 'q'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x0155, 'r'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x0159, 'r'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x0157, 'r'); // WITH CEDILLA, LATIN SMALL LETTER
normalized.put((char)0x1E59, 'r'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E5B, 'r'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0211, 'r'); // WITH DOUBLE GRAVE, LATIN SMALL LETTER
normalized.put((char)0x027E, 'r'); // WITH FISHHOOK, LATIN SMALL LETTER
normalized.put((char)0x027F, 'r'); // WITH FISHHOOK, LATIN SMALL LETTER REVERSED
normalized.put((char)0x027B, 'r'); // WITH HOOK, LATIN SMALL LETTER TURNED
normalized.put((char)0x0213, 'r'); // WITH INVERTED BREVE, LATIN SMALL LETTER
normalized.put((char)0x1E5F, 'r'); // WITH LINE BELOW, LATIN SMALL LETTER
normalized.put((char)0x027C, 'r'); // WITH LONG LEG, LATIN SMALL LETTER
normalized.put((char)0x027A, 'r'); // WITH LONG LEG, LATIN SMALL LETTER TURNED
normalized.put((char)0x024D, 'r'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x027D, 'r'); // WITH TAIL, LATIN SMALL LETTER
normalized.put((char)0x036C, 'r'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x0279, 'r'); // , LATIN SMALL LETTER TURNED
normalized.put((char)0x1D63, 'r'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x015B, 's'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x0161, 's'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x015F, 's'); // WITH CEDILLA, LATIN SMALL LETTER
normalized.put((char)0x015D, 's'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x0219, 's'); // WITH COMMA BELOW, LATIN SMALL LETTER
normalized.put((char)0x1E61, 's'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E9B, 's'); // WITH DOT ABOVE, LATIN SMALL LETTER LONG
normalized.put((char)0x1E63, 's'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0282, 's'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x023F, 's'); // WITH SWASH TAIL, LATIN SMALL LETTER
normalized.put((char)0x017F, 's'); // , LATIN SMALL LETTER LONG
normalized.put((char)0x00DF, 's'); // , LATIN SMALL LETTER SHARP
normalized.put((char)0x209B, 's'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x0165, 't'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x0163, 't'); // WITH CEDILLA, LATIN SMALL LETTER
normalized.put((char)0x1E71, 't'); // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
normalized.put((char)0x021B, 't'); // WITH COMMA BELOW, LATIN SMALL LETTER
normalized.put((char)0x0236, 't'); // WITH CURL, LATIN SMALL LETTER
normalized.put((char)0x1E97, 't'); // WITH DIAERESIS, LATIN SMALL LETTER
normalized.put((char)0x1E6B, 't'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E6D, 't'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x01AD, 't'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x1E6F, 't'); // WITH LINE BELOW, LATIN SMALL LETTER
normalized.put((char)0x01AB, 't'); // WITH PALATAL HOOK, LATIN SMALL LETTER
normalized.put((char)0x0288, 't'); // WITH RETROFLEX HOOK, LATIN SMALL LETTER
normalized.put((char)0x0167, 't'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x036D, 't'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x0287, 't'); // , LATIN SMALL LETTER TURNED
normalized.put((char)0x209C, 't'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x0289, 'u'); // BAR, LATIN SMALL LETTER
normalized.put((char)0x00FA, 'u'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x016D, 'u'); // WITH BREVE, LATIN SMALL LETTER
normalized.put((char)0x01D4, 'u'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x1E77, 'u'); // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
normalized.put((char)0x00FB, 'u'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x1E73, 'u'); // WITH DIAERESIS BELOW, LATIN SMALL LETTER
normalized.put((char)0x00FC, 'u'); // WITH DIAERESIS, LATIN SMALL LETTER
normalized.put((char)0x1EE5, 'u'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0171, 'u'); // WITH DOUBLE ACUTE, LATIN SMALL LETTER
normalized.put((char)0x0215, 'u'); // WITH DOUBLE GRAVE, LATIN SMALL LETTER
normalized.put((char)0x00F9, 'u'); // WITH GRAVE, LATIN SMALL LETTER
normalized.put((char)0x1EE7, 'u'); // WITH HOOK ABOVE, LATIN SMALL LETTER
normalized.put((char)0x01B0, 'u'); // WITH HORN, LATIN SMALL LETTER
normalized.put((char)0x0217, 'u'); // WITH INVERTED BREVE, LATIN SMALL LETTER
normalized.put((char)0x016B, 'u'); // WITH MACRON, LATIN SMALL LETTER
normalized.put((char)0x0173, 'u'); // WITH OGONEK, LATIN SMALL LETTER
normalized.put((char)0x016F, 'u'); // WITH RING ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E75, 'u'); // WITH TILDE BELOW, LATIN SMALL LETTER
normalized.put((char)0x0169, 'u'); // WITH TILDE, LATIN SMALL LETTER
normalized.put((char)0x0367, 'u'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x1D1D, 'u'); // , LATIN SMALL LETTER SIDEWAYS
normalized.put((char)0x1D1E, 'u'); // , LATIN SMALL LETTER SIDEWAYS DIAERESIZED
normalized.put((char)0x1D64, 'u'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x1E7F, 'v'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x028B, 'v'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x1E7D, 'v'); // WITH TILDE, LATIN SMALL LETTER
normalized.put((char)0x036E, 'v'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x028C, 'v'); // , LATIN SMALL LETTER TURNED
normalized.put((char)0x1D65, 'v'); // , LATIN SUBSCRIPT SMALL LETTER
normalized.put((char)0x1E83, 'w'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x0175, 'w'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x1E85, 'w'); // WITH DIAERESIS, LATIN SMALL LETTER
normalized.put((char)0x1E87, 'w'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E89, 'w'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x1E81, 'w'); // WITH GRAVE, LATIN SMALL LETTER
normalized.put((char)0x1E98, 'w'); // WITH RING ABOVE, LATIN SMALL LETTER
normalized.put((char)0x028D, 'w'); // , LATIN SMALL LETTER TURNED
normalized.put((char)0x1E8D, 'x'); // WITH DIAERESIS, LATIN SMALL LETTER
normalized.put((char)0x1E8B, 'x'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x036F, 'x'); // , COMBINING LATIN SMALL LETTER
normalized.put((char)0x00FD, 'y'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x0177, 'y'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x00FF, 'y'); // WITH DIAERESIS, LATIN SMALL LETTER
normalized.put((char)0x1E8F, 'y'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1EF5, 'y'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x1EF3, 'y'); // WITH GRAVE, LATIN SMALL LETTER
normalized.put((char)0x1EF7, 'y'); // WITH HOOK ABOVE, LATIN SMALL LETTER
normalized.put((char)0x01B4, 'y'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x0233, 'y'); // WITH MACRON, LATIN SMALL LETTER
normalized.put((char)0x1E99, 'y'); // WITH RING ABOVE, LATIN SMALL LETTER
normalized.put((char)0x024F, 'y'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x1EF9, 'y'); // WITH TILDE, LATIN SMALL LETTER
normalized.put((char)0x028E, 'y'); // , LATIN SMALL LETTER TURNED
normalized.put((char)0x017A, 'z'); // WITH ACUTE, LATIN SMALL LETTER
normalized.put((char)0x017E, 'z'); // WITH CARON, LATIN SMALL LETTER
normalized.put((char)0x1E91, 'z'); // WITH CIRCUMFLEX, LATIN SMALL LETTER
normalized.put((char)0x0291, 'z'); // WITH CURL, LATIN SMALL LETTER
normalized.put((char)0x017C, 'z'); // WITH DOT ABOVE, LATIN SMALL LETTER
normalized.put((char)0x1E93, 'z'); // WITH DOT BELOW, LATIN SMALL LETTER
normalized.put((char)0x0225, 'z'); // WITH HOOK, LATIN SMALL LETTER
normalized.put((char)0x1E95, 'z'); // WITH LINE BELOW, LATIN SMALL LETTER
normalized.put((char)0x0290, 'z'); // WITH RETROFLEX HOOK, LATIN SMALL LETTER
normalized.put((char)0x01B6, 'z'); // WITH STROKE, LATIN SMALL LETTER
normalized.put((char)0x0240, 'z'); // WITH SWASH TAIL, LATIN SMALL LETTER
normalized.put((char)0x0251, 'a'); // , latin small letter script
normalized.put((char)0x00C1, 'A'); // WITH ACUTE, LATIN CAPITAL LETTER
normalized.put((char)0x00C2, 'A'); // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
normalized.put((char)0x00C4, 'A'); // WITH DIAERESIS, LATIN CAPITAL LETTER
normalized.put((char)0x00C0, 'A'); // WITH GRAVE, LATIN CAPITAL LETTER
normalized.put((char)0x00C5, 'A'); // WITH RING ABOVE, LATIN CAPITAL LETTER
normalized.put((char)0x023A, 'A'); // WITH STROKE, LATIN CAPITAL LETTER
normalized.put((char)0x00C3, 'A'); // WITH TILDE, LATIN CAPITAL LETTER
normalized.put((char)0x1D00, 'A'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x0181, 'B'); // WITH HOOK, LATIN CAPITAL LETTER
normalized.put((char)0x0243, 'B'); // WITH STROKE, LATIN CAPITAL LETTER
normalized.put((char)0x0299, 'B'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x1D03, 'B'); // , LATIN LETTER SMALL CAPITAL BARRED
normalized.put((char)0x00C7, 'C'); // WITH CEDILLA, LATIN CAPITAL LETTER
normalized.put((char)0x023B, 'C'); // WITH STROKE, LATIN CAPITAL LETTER
normalized.put((char)0x1D04, 'C'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x018A, 'D'); // WITH HOOK, LATIN CAPITAL LETTER
normalized.put((char)0x0189, 'D'); // , LATIN CAPITAL LETTER AFRICAN
normalized.put((char)0x1D05, 'D'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x00C9, 'E'); // WITH ACUTE, LATIN CAPITAL LETTER
normalized.put((char)0x00CA, 'E'); // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
normalized.put((char)0x00CB, 'E'); // WITH DIAERESIS, LATIN CAPITAL LETTER
normalized.put((char)0x00C8, 'E'); // WITH GRAVE, LATIN CAPITAL LETTER
normalized.put((char)0x0246, 'E'); // WITH STROKE, LATIN CAPITAL LETTER
normalized.put((char)0x0190, 'E'); // , LATIN CAPITAL LETTER OPEN
normalized.put((char)0x018E, 'E'); // , LATIN CAPITAL LETTER REVERSED
normalized.put((char)0x1D07, 'E'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x0193, 'G'); // WITH HOOK, LATIN CAPITAL LETTER
normalized.put((char)0x029B, 'G'); // WITH HOOK, LATIN LETTER SMALL CAPITAL
normalized.put((char)0x0262, 'G'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x029C, 'H'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x00CD, 'I'); // WITH ACUTE, LATIN CAPITAL LETTER
normalized.put((char)0x00CE, 'I'); // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
normalized.put((char)0x00CF, 'I'); // WITH DIAERESIS, LATIN CAPITAL LETTER
normalized.put((char)0x0130, 'I'); // WITH DOT ABOVE, LATIN CAPITAL LETTER
normalized.put((char)0x00CC, 'I'); // WITH GRAVE, LATIN CAPITAL LETTER
normalized.put((char)0x0197, 'I'); // WITH STROKE, LATIN CAPITAL LETTER
normalized.put((char)0x026A, 'I'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x0248, 'J'); // WITH STROKE, LATIN CAPITAL LETTER
normalized.put((char)0x1D0A, 'J'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x1D0B, 'K'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x023D, 'L'); // WITH BAR, LATIN CAPITAL LETTER
normalized.put((char)0x1D0C, 'L'); // WITH STROKE, LATIN LETTER SMALL CAPITAL
normalized.put((char)0x029F, 'L'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x019C, 'M'); // , LATIN CAPITAL LETTER TURNED
normalized.put((char)0x1D0D, 'M'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x019D, 'N'); // WITH LEFT HOOK, LATIN CAPITAL LETTER
normalized.put((char)0x0220, 'N'); // WITH LONG RIGHT LEG, LATIN CAPITAL LETTER
normalized.put((char)0x00D1, 'N'); // WITH TILDE, LATIN CAPITAL LETTER
normalized.put((char)0x0274, 'N'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x1D0E, 'N'); // , LATIN LETTER SMALL CAPITAL REVERSED
normalized.put((char)0x00D3, 'O'); // WITH ACUTE, LATIN CAPITAL LETTER
normalized.put((char)0x00D4, 'O'); // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
normalized.put((char)0x00D6, 'O'); // WITH DIAERESIS, LATIN CAPITAL LETTER
normalized.put((char)0x00D2, 'O'); // WITH GRAVE, LATIN CAPITAL LETTER
normalized.put((char)0x019F, 'O'); // WITH MIDDLE TILDE, LATIN CAPITAL LETTER
normalized.put((char)0x00D8, 'O'); // WITH STROKE, LATIN CAPITAL LETTER
normalized.put((char)0x00D5, 'O'); // WITH TILDE, LATIN CAPITAL LETTER
normalized.put((char)0x0186, 'O'); // , LATIN CAPITAL LETTER OPEN
normalized.put((char)0x1D0F, 'O'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x1D10, 'O'); // , LATIN LETTER SMALL CAPITAL OPEN
normalized.put((char)0x1D18, 'P'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x024A, 'Q'); // WITH HOOK TAIL, LATIN CAPITAL LETTER SMALL
normalized.put((char)0x024C, 'R'); // WITH STROKE, LATIN CAPITAL LETTER
normalized.put((char)0x0280, 'R'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x0281, 'R'); // , LATIN LETTER SMALL CAPITAL INVERTED
normalized.put((char)0x1D19, 'R'); // , LATIN LETTER SMALL CAPITAL REVERSED
normalized.put((char)0x1D1A, 'R'); // , LATIN LETTER SMALL CAPITAL TURNED
normalized.put((char)0x023E, 'T'); // WITH DIAGONAL STROKE, LATIN CAPITAL LETTER
normalized.put((char)0x01AE, 'T'); // WITH RETROFLEX HOOK, LATIN CAPITAL LETTER
normalized.put((char)0x1D1B, 'T'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x0244, 'U'); // BAR, LATIN CAPITAL LETTER
normalized.put((char)0x00DA, 'U'); // WITH ACUTE, LATIN CAPITAL LETTER
normalized.put((char)0x00DB, 'U'); // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
normalized.put((char)0x00DC, 'U'); // WITH DIAERESIS, LATIN CAPITAL LETTER
normalized.put((char)0x00D9, 'U'); // WITH GRAVE, LATIN CAPITAL LETTER
normalized.put((char)0x1D1C, 'U'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x01B2, 'V'); // WITH HOOK, LATIN CAPITAL LETTER
normalized.put((char)0x0245, 'V'); // , LATIN CAPITAL LETTER TURNED
normalized.put((char)0x1D20, 'V'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x1D21, 'W'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x00DD, 'Y'); // WITH ACUTE, LATIN CAPITAL LETTER
normalized.put((char)0x0178, 'Y'); // WITH DIAERESIS, LATIN CAPITAL LETTER
normalized.put((char)0x024E, 'Y'); // WITH STROKE, LATIN CAPITAL LETTER
normalized.put((char)0x028F, 'Y'); // , LATIN LETTER SMALL CAPITAL
normalized.put((char)0x1D22, 'Z'); // , LATIN LETTER SMALL CAPITAL
normalized.put('Ắ', 'A');
normalized.put('Ấ', 'A');
normalized.put('Ằ', 'A');
normalized.put('Ầ', 'A');
normalized.put('Ẳ', 'A');
normalized.put('Ẩ', 'A');
normalized.put('Ẵ', 'A');
normalized.put('Ẫ', 'A');
normalized.put('Ặ', 'A');
normalized.put('Ậ', 'A');
normalized.put('ắ', 'a');
normalized.put('ấ', 'a');
normalized.put('ằ', 'a');
normalized.put('ầ', 'a');
normalized.put('ẳ', 'a');
normalized.put('ẩ', 'a');
normalized.put('ẵ', 'a');
normalized.put('ẫ', 'a');
normalized.put('ặ', 'a');
normalized.put('ậ', 'a');
normalized.put('Ế', 'E');
normalized.put('Ề', 'E');
normalized.put('Ể', 'E');
normalized.put('Ễ', 'E');
normalized.put('Ệ', 'E');
normalized.put('ế', 'e');
normalized.put('ề', 'e');
normalized.put('ể', 'e');
normalized.put('ễ', 'e');
normalized.put('ệ', 'e');
normalized.put('Ố', 'O');
normalized.put('Ớ', 'O');
normalized.put('Ồ', 'O');
normalized.put('Ờ', 'O');
normalized.put('Ổ', 'O');
normalized.put('Ở', 'O');
normalized.put('Ỗ', 'O');
normalized.put('Ỡ', 'O');
normalized.put('Ộ', 'O');
normalized.put('Ợ', 'O');
normalized.put('ố', 'o');
normalized.put('ớ', 'o');
normalized.put('ồ', 'o');
normalized.put('ờ', 'o');
normalized.put('ổ', 'o');
normalized.put('ở', 'o');
normalized.put('ỗ', 'o');
normalized.put('ỡ', 'o');
normalized.put('ộ', 'o');
normalized.put('ợ', 'o');
normalized.put('Ứ', 'U');
normalized.put('Ừ', 'U');
normalized.put('Ử', 'U');
normalized.put('Ữ', 'U');
normalized.put('Ự', 'U');
normalized.put('ứ', 'u');
normalized.put('ừ', 'u');
normalized.put('ử', 'u');
normalized.put('ữ', 'u');
normalized.put('ự', 'u');
}
public static String normalizeRunes(String text) {
char[] chars = text.toCharArray();
for (int i = 0; i < chars.length; i++) {
char r = chars[i];
if (r < 0x00C0 || r > 0x2184) {
continue;
}
Character n = normalized.get(r);
if (n != null && r > 0) {
chars[i] = n;
}
}
return new String(chars);
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
/**
* Interface defining a search match for {@code text} agains {@code pattern}.
* Resulting match result gives information in a context of {@code text} how and
* where matches happens and provides a {@code score} number which can be to
* sort results.
*
* @author Janne Valkealahti
*/
@FunctionalInterface
public interface SearchMatch {
/**
* Match a pattern into a given text.
*
* @param text the text to search
* @param pattern the search pattern
* @return a result
*/
SearchMatchResult match(String text, String pattern);
/**
* Gets an instance of a builder for a {@link SearchMatch}.
*
* @return builder for search match
*/
public static Builder builder() {
return new DefaultBuilder();
}
/**
* Defines an interface for {@link SearchMatch}.
*/
interface Builder {
/**
* Set a flag for {@code caseSensitive}.
*
* @param caseSensitive the caseSensitive
* @return builder for chaining
*/
Builder caseSensitive(boolean caseSensitive);
/**
* Set a flag for {@code normalize}.
*
* @param normalize the normalize
* @return builder for chaining
*/
Builder normalize(boolean normalize);
/**
* Set a flag for {@code forward}.
*
* @param forward the forward
* @return builder for chaining
*/
Builder forward(boolean forward);
/**
* Build instance of a {@link SearchMatch}.
*
* @return a build instance of {@link SearchMatch}
*/
SearchMatch build();
}
static class DefaultBuilder implements Builder {
private boolean caseSensitive;
private boolean normalize;
private boolean forward;
@Override
public Builder caseSensitive(boolean caseSensitive) {
this.caseSensitive = caseSensitive;
return this;
}
@Override
public Builder normalize(boolean normalize) {
this.normalize = normalize;
return this;
}
@Override
public Builder forward(boolean forward) {
this.forward = forward;
return this;
}
@Override
public SearchMatch build() {
return new DefaultSearchMatch(this.caseSensitive, this.normalize, this.forward);
}
}
static class DefaultSearchMatch implements SearchMatch {
private boolean caseSensitive;
private boolean normalize;
private boolean forward;
DefaultSearchMatch(boolean caseSensitive, boolean normalize, boolean forward) {
this.caseSensitive = caseSensitive;
this.normalize = normalize;
this.forward = forward;
}
@Override
public SearchMatchResult match(String text, String pattern) {
SearchMatchAlgorithm algo = null;
if (pattern != null) {
// algos are currently expecting to pass pattern as lower case if
// case sensitivity is not enabled.
if (!caseSensitive) {
pattern = pattern.toLowerCase();
}
// algos are currently expecting to pass pattern as normalized if
// it is enabled.
if (normalize) {
pattern = Normalize.normalizeRunes(pattern);
}
// pick algorithm based on pattern
if (pattern.startsWith("'")) {
// exact match starting with "'"
algo = new ExactMatchNaiveSearchMatchAlgorithm();
pattern = pattern.substring(1);
}
}
// default algo is fuzzy match
if (algo == null) {
// algo = new FuzzyMatchV1SearchMatchAlgorithm();
algo = new FuzzyMatchV2SearchMatchAlgorithm();
}
return algo.match(caseSensitive, normalize, forward, text, pattern);
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
/**
* Interface to match given text with a pattern into a result.
*
* @author Janne Valkealahti
*/
@FunctionalInterface
public interface SearchMatchAlgorithm {
/**
* Match given text with pattern as a result.
*
* @param caseSensitive the caseSensitive
* @param normalize the normalize
* @param forward the forward
* @param text the test
* @param pattern the pattern
* @return a search match result
*/
SearchMatchResult match(boolean caseSensitive, boolean normalize, boolean forward, String text, String pattern);
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
/**
* Interface defining result used in {@link SearchMatch}.
*
* @author Janne Valkealahti
*/
public interface SearchMatchResult {
/**
* Get start of a match.
*
* @return a start of a match
*/
int getStart();
/**
* Get end of a match.
*
* @return a end of a matchh
*/
int getEnd();
/**
* Get score of a match.
*
* @return a score of a match
*/
int getScore();
/**
* Get positions of a match.
*
* @return a positions of a match
*/
int[] getPositions();
/**
* Get {@link SearchMatchAlgorithm} handling a search.
*
* @return {@link SearchMatchAlgorithm} handling a search
*/
SearchMatchAlgorithm getAlgorithm();
/**
* Construct {@link SearchMatchResult} with given parameters.
*
* @param start the start
* @param end the end
* @param score the score
* @param positions the positions
* @return a search match result
*/
public static SearchMatchResult of(int start, int end, int score, int[] positions, SearchMatchAlgorithm algo) {
return new DefaultResult(start, end, score, positions, algo);
}
public static SearchMatchResult ofZeros() {
return of(0, 0, 0, new int[0], null);
}
public static SearchMatchResult ofMinus() {
return of(-1, -1, 0, new int[0], null);
}
static class DefaultResult implements SearchMatchResult {
int start;
int end;
int score;
int[] positions;
SearchMatchAlgorithm algo;
DefaultResult(int start, int end, int score, int[] positions, SearchMatchAlgorithm algo) {
this.start = start;
this.end = end;
this.score = score;
this.positions = positions;
this.algo = algo;
}
@Override
public int getStart() {
return start;
}
@Override
public int getEnd() {
return end;
}
@Override
public int getScore() {
return score;
}
@Override
public int[] getPositions() {
return positions;
}
@Override
public SearchMatchAlgorithm getAlgorithm() {
return algo;
}
}
}

View File

@@ -0,0 +1,53 @@
// message
message(model) ::= <%
<if(model.message && model.hasMessageLevelError)>
<({<figures.error>}); format="style-level-error"> <model.message; format="style-level-error">
<elseif(model.message && model.hasMessageLevelWarn)>
<({<figures.warning>}); format="style-level-warn"> <model.message; format="style-level-warn">
<elseif(model.message && model.hasMessageLevelInfo)>
<({<figures.info>}); format="style-level-info"> <model.message; format="style-level-info">
<endif>
%>
// info section after '? xxx'
info(model) ::= <%
<if(model.input)>
<model.input>
<endif>
%>
// start '? xxx' shows both running and result
question_name(model) ::= <<
<({<figures.questionMark>}); format="style-list-value"> <model.name; format="style-title">
>>
// render path item
path_item(item,model) ::= <%
<if(item.selected)>
<({<figures.rightPointingQuotation> }); format="style-item-selector"><item.partsText; format={width:<model.terminalWidth>,prefix:2,textStyle:style-item-selector,matchStyle:style-level-warn}>
<else>
<(" ")><item.partsText; format={width:<model.terminalWidth>,prefix:2,textStyle:style-item-selector,matchStyle:style-level-warn}>
<endif>
%>
// render paths
paths(model) ::= <<
<model.pathViewItems:{x|<path_item(x,model)>}; separator="\n">
>>
// component result
result(model) ::= <<
<question_name(model)> <model.resultValue; format="style-value">
>>
// component is running
running(model) ::= <<
<question_name(model)> <info(model)>
<message(model)>
<paths(model)>
>>
// main
main(model) ::= <<
<if(model.resultValue)><result(model)><else><running(model)><endif>
>>

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.component;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.shell.component.PathSearch.PathSearchContext;
import org.springframework.shell.style.PartsText;
import org.springframework.shell.style.PartsText.PartText;
import static org.assertj.core.api.Assertions.assertThat;
public class PathSearchTests {
static Stream<Arguments> testOfNameMatchParts() {
return Stream.of(
Arguments.of("0", new int[] { 0 },
Arrays.asList(PartText.of("0", true))),
Arguments.of("01", new int[] { 0, 1 },
Arrays.asList(
PartText.of("0", true),
PartText.of("1", true))),
Arguments.of("012", new int[] { 0, 1, 2 },
Arrays.asList(
PartText.of("0", true),
PartText.of("1", true),
PartText.of("2", true))),
Arguments.of("0123456789", new int[0],
Arrays.asList(PartText.of("0123456789", false))),
Arguments.of("0123456789", new int[] { 0 },
Arrays.asList(
PartText.of("0", true),
PartText.of("123456789", false))),
Arguments.of("0123456789", new int[] { 1 },
Arrays.asList(
PartText.of("0", false),
PartText.of("1", true),
PartText.of("23456789", false))),
Arguments.of("0123456789", new int[] { 9 },
Arrays.asList(
PartText.of("012345678", false),
PartText.of("9", true))),
Arguments.of("0123456789", new int[] { 2, 5 },
Arrays.asList(
PartText.of("01", false),
PartText.of("2", true),
PartText.of("34", false),
PartText.of("5", true),
PartText.of("6789", false))),
Arguments.of("0123456789", new int[] { 2, 3 },
Arrays.asList(
PartText.of("01", false),
PartText.of("2", true),
PartText.of("3", true),
PartText.of("456789", false))),
Arguments.of("0123456789", new int[] { 8, 9 },
Arrays.asList(
PartText.of("01234567", false),
PartText.of("8", true),
PartText.of("9", true)))
);
}
@ParameterizedTest
@MethodSource
void testOfNameMatchParts(String text, int[] positions, List<PartText> parts) {
PartsText ofNameMatchPartsx = PathSearchContext.ofNameMatchPartsx(text, positions);
List<PartText> res = ofNameMatchPartsx.getParts();
assertThat(res).hasSize(parts.size());
for (int i = 0; i < parts.size(); i++) {
assertThat(res.get(i).getText()).isEqualTo(parts.get(i).getText());
assertThat(res.get(i).isMatch()).isEqualTo(parts.get(i).isMatch());
}
}
}

View File

@@ -0,0 +1,309 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.component.support;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.InstanceOfAssertFactory;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class SelectorListTests {
@Test
void hasCorrectInitialStateWhenResetZero() {
SelectorList<TestItem> list = SelectorList.of(5);
list.reset(Collections.emptyList());
assertThat(list).asInstanceOf(SELECTOR_LIST).hasProjectionSize(0);
}
@Test
void hasCorrectInitialState() {
SelectorList<TestItem> list = SelectorList.of(5);
list.reset(items(5));
assertThat(list.getSelected().getName()).isEqualTo("name0");
assertThat(list).asInstanceOf(SELECTOR_LIST)
.hasProjectionSize(5)
.namesContainsExactly("name0", "name1", "name2", "name3", "name4")
.selectedContainsExactly(true, false, false, false, false);
}
@Test
void scrollDownLessItemsThanMax() {
SelectorList<TestItem> list = SelectorList.of(3);
list.reset(items(3));
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(true, false, false);
list.scrollDown();
assertThat(list.getSelected().getName()).isEqualTo("name1");
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(false, true, false);
list.scrollDown();
assertThat(list.getSelected().getName()).isEqualTo("name2");
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(false, false, true);
list.scrollDown();
assertThat(list.getSelected().getName()).isEqualTo("name0");
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(true, false, false);
list.scrollDown();
assertThat(list.getSelected().getName()).isEqualTo("name1");
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(false, true, false);
list.scrollDown();
assertThat(list.getSelected().getName()).isEqualTo("name2");
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(false, false, true);
}
@Test
void scrollUpLessItemsThanMax() {
SelectorList<TestItem> list = SelectorList.of(3);
list.reset(items(3));
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(true, false, false);
list.scrollUp();
assertThat(list.getSelected().getName()).isEqualTo("name2");
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(false, false, true);
list.scrollUp();
assertThat(list.getSelected().getName()).isEqualTo("name1");
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(false, true, false);
list.scrollUp();
assertThat(list.getSelected().getName()).isEqualTo("name0");
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(true, false, false);
list.scrollUp();
assertThat(list.getSelected().getName()).isEqualTo("name2");
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(false, false, true);
list.scrollUp();
assertThat(list.getSelected().getName()).isEqualTo("name1");
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(false, true, false);
}
@Test
void scrollDownMoreItemsThanMax() {
SelectorList<TestItem> list = SelectorList.of(3);
list.reset(items(5));
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(true, false, false);
list.scrollDown();
list.scrollDown();
list.scrollDown();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name1", "name2", "name3")
.selectedContainsExactly(false, false, true);
list.scrollDown();
list.scrollDown();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(true, false, false);
}
@Test
void scrollUpMoreItemsThanMax() {
SelectorList<TestItem> list = SelectorList.of(3);
list.reset(items(5));
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(true, false, false);
list.scrollUp();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name2", "name3", "name4")
.selectedContainsExactly(false, false, true);
list.scrollUp();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name2", "name3", "name4")
.selectedContainsExactly(false, true, false);
list.scrollUp();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name2", "name3", "name4")
.selectedContainsExactly(true, false, false);
list.scrollUp();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name1", "name2", "name3")
.selectedContainsExactly(true, false, false);
list.scrollUp();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1", "name2")
.selectedContainsExactly(true, false, false);
}
@Test
void scrollUpAndDownMoreItemsThanMax() {
SelectorList<TestItem> list = SelectorList.of(3);
list.reset(items(5));
list.scrollUp();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name2", "name3", "name4")
.selectedContainsExactly(false, false, true);
list.scrollUp();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name2", "name3", "name4")
.selectedContainsExactly(false, true, false);
list.scrollDown();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name2", "name3", "name4")
.selectedContainsExactly(false, false, true);
}
@Test
void scrollUpAndDownMoreItemsThanMax2() {
SelectorList<TestItem> list = SelectorList.of(3);
list.reset(items(5));
list.scrollUp();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name2", "name3", "name4")
.selectedContainsExactly(false, false, true);
list.scrollUp();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name2", "name3", "name4")
.selectedContainsExactly(false, true, false);
list.scrollUp();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name2", "name3", "name4")
.selectedContainsExactly(true, false, false);
list.scrollDown();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name2", "name3", "name4")
.selectedContainsExactly(false, true, false);
}
@Test
void upWhenLessItemsThanMax() {
SelectorList<TestItem> list = SelectorList.of(3);
list.reset(items(2));
list.scrollUp();
assertThat(list).asInstanceOf(SELECTOR_LIST)
.namesContainsExactly("name0", "name1")
.selectedContainsExactly(false, true);
}
private static class TestItem implements Nameable {
String name;
TestItem(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
List<TestItem> items(int count) {
return IntStream.range(0, count)
.mapToObj(i -> {
return new TestItem("name" + i);
})
.collect(Collectors.toList());
}
@SuppressWarnings("rawtypes")
InstanceOfAssertFactory<SelectorList, SelectorListAssert<TestItem>> SELECTOR_LIST = selectorList(TestItem.class);
@SuppressWarnings("rawtypes")
static <RESULT extends Nameable> InstanceOfAssertFactory<SelectorList, SelectorListAssert<RESULT>> selectorList(Class<RESULT> resultType) {
return new InstanceOfAssertFactory<>(SelectorList.class, SelectorListAssertions::<RESULT> assertThat);
}
static class SelectorListAssertions {
public static <T extends Nameable> SelectorListAssert<T> assertThat(SelectorList<T> actual) {
return new SelectorListAssert<>(actual);
}
}
static class SelectorListAssert<T extends Nameable> extends AbstractAssert<SelectorListAssert<T>, SelectorList<T>> {
public SelectorListAssert(SelectorList<T> actual) {
super(actual, SelectorListAssert.class);
}
public SelectorListAssert<T> namesContainsExactly(String... names) {
isNotNull();
List<String> actualNames = actual.getProjection().stream()
.map(i -> i.getName())
.collect(Collectors.toList());
assertThat(actualNames).containsExactly(names);
return this;
}
public SelectorListAssert<T> selectedContainsExactly(Boolean... selected) {
isNotNull();
List<Boolean> actualSelected = actual.getProjection().stream()
.map(i -> i.isSelected())
.collect(Collectors.toList());
assertThat(actualSelected).containsExactly(selected);
return this;
}
public SelectorListAssert<T> hasProjectionSize(int size) {
isNotNull();
assertThat(actual.getProjection()).hasSize(size);
return this;
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.style;
import java.util.Locale;
import java.util.stream.Stream;
import org.jline.utils.AttributedString;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.shell.style.PartsText.PartText;
import static org.assertj.core.api.Assertions.assertThat;
class PartsTextRendererTests {
private static Locale LOCALE = Locale.getDefault();
private static PartsTextRenderer renderer;
private static ThemeResolver themeResolver;
@BeforeAll
static void setup() {
ThemeRegistry themeRegistry = new ThemeRegistry();
themeRegistry.register(new Theme() {
@Override
public String getName() {
return "default";
}
@Override
public ThemeSettings getSettings() {
return ThemeSettings.defaults();
}
});
themeResolver = new ThemeResolver(themeRegistry, "default");
renderer = new PartsTextRenderer(themeResolver);
}
static PartsText of() {
return PartsText.of(
PartText.of("012", false),
PartText.of("3456", true),
PartText.of("789", false)
);
}
static Stream<Arguments> test() {
return Stream.of(
Arguments.of(
"width:10,prefix:2,textStyle:style-item-selector,matchStyle:style-level-warn",
PartsText.of(
PartText.of("01234567", false)
),
"01234567"),
Arguments.of(
"width:10,prefix:2,textStyle:style-item-selector,matchStyle:style-level-warn",
PartsText.of(
PartText.of("0123456789", false)
),
"012345.."),
Arguments.of(
"width:10,prefix:0,textStyle:style-item-selector,matchStyle:style-level-warn",
PartsText.of(
PartText.of("01234", false),
PartText.of("56789", true)
),
"0123456789"),
Arguments.of(
"width:10,prefix:2,textStyle:style-item-selector,matchStyle:style-level-warn",
PartsText.of(
PartText.of("012", false),
PartText.of("3456", true),
PartText.of("789", false)
),
"012345..")
);
}
@ParameterizedTest
@MethodSource
void test(String expression, PartsText text, String expected) {
String rendered = renderer.toString(text, expression, LOCALE);
AttributedString evaluated = themeResolver.evaluateExpression(rendered);
String raw = AttributedString.stripAnsi(evaluated.toAnsi());
assertThat(raw).isEqualTo(expected);
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_BOUNDARY;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_BOUNDARY_DELIMITER;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_CAMEL123;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_CONSECUTIVE;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_FIRST_CHAR_MULTIPLIER;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.SCORE_MATCH;
class ExactMatchNaiveSearchMatchAlgorithmTests {
static Stream<Arguments> testExactMatch() {
return Stream.of(
Arguments.of(true, false, true, "fooBarbaz", "oBA", false, -1, -1, new int[] {},
0),
Arguments.of(true, false, true, "fooBarbaz", "fooBarbazz", false, -1, -1, new int[] {},
0),
Arguments.of(false, false, true, "fooBarbaz", "oBA", false, 2, 5, new int[] { 2, 3, 4 },
SCORE_MATCH * 3 + BONUS_CAMEL123 + BONUS_CONSECUTIVE),
Arguments.of(false, false, true, "/AutomatorDocument.icns", "rdoc", false, 9, 13, new int[] { 9, 10, 11, 12 },
SCORE_MATCH * 4 + BONUS_CAMEL123 + BONUS_CONSECUTIVE * 2),
Arguments.of(false, false, true, "/man1/zshcompctl.1", "zshc", false, 6, 10, new int[] { 6, 7, 8, 9 },
SCORE_MATCH * 4 + BONUS_BOUNDARY_DELIMITER * (BONUS_FIRST_CHAR_MULTIPLIER + 3)),
Arguments.of(false, false, true, "/.oh-my-zsh/cache", "zsh/c", false, 8, 13, new int[] { 8, 9, 10, 11, 12 },
SCORE_MATCH * 5 + BONUS_BOUNDARY * (BONUS_FIRST_CHAR_MULTIPLIER + 3) + BONUS_BOUNDARY_DELIMITER),
Arguments.of(false, false, true, "fooBarbaz", "o", false, 1, 2, new int[] { 1 },
SCORE_MATCH * 1),
Arguments.of(false, false, true, "/tmp/test/11/file11.txt", "e", false, 6, 7, new int[] { 6 },
SCORE_MATCH * 1),
Arguments.of(false, true, true, "Só Danço Samba", "So", false, 0, 2, new int[] { 0, 1 },
62),
Arguments.of(false, true, true, "Danço", "danco", false, 0, 5, new int[] { 0, 1, 2, 3, 4 },
140)
);
}
@ParameterizedTest
@MethodSource
void testExactMatch(boolean caseSensitive, boolean normalize, boolean forward, String text, String pattern,
boolean withPos, int start, int end, int[] positions, int score) {
if (!caseSensitive) {
pattern = pattern.toLowerCase();
}
ExactMatchNaiveSearchMatchAlgorithm searchMatch = new ExactMatchNaiveSearchMatchAlgorithm();
SearchMatchResult result = searchMatch.match(caseSensitive, normalize, forward, text, pattern);
assertThat(result.getStart()).isEqualTo(start);
assertThat(result.getEnd()).isEqualTo(end);
assertThat(result.getScore()).isEqualTo(score);
assertThat(result.getPositions()).containsExactly(positions);
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_BOUNDARY;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_BOUNDARY_DELIMITER;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_BOUNDARY_WHITE;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_CAMEL123;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_CONSECUTIVE;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_FIRST_CHAR_MULTIPLIER;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_NON_WORD;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.SCORE_GAP_EXTENSION;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.SCORE_GAP_START;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.SCORE_MATCH;
class FuzzyMatchV1SearchMatchAlgorithmTests {
private final FuzzyMatchV1SearchMatchAlgorithm searchMatch = new FuzzyMatchV1SearchMatchAlgorithm();
static Stream<Arguments> testFuzzyMatchV1() {
return Stream.of(
Arguments.of(false, false, "fooBarbaz1", "oBZ", true, 2, 9, new int[] { 2, 3, 8 },
SCORE_MATCH * 3 + BONUS_CAMEL123 + SCORE_GAP_START + SCORE_GAP_EXTENSION * 3),
Arguments.of(false, false, "foo bar baz", "fbb", true, 0, 9, new int[] { 0, 4, 8 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE * 2 +
2 * SCORE_GAP_START + 4 * SCORE_GAP_EXTENSION),
Arguments.of(false, false, "/AutomatorDocument.icns", "rdoc", true, 9, 13, new int[] { 9, 10, 11, 12 },
SCORE_MATCH * 4 + BONUS_CAMEL123 + BONUS_CONSECUTIVE * 2),
Arguments.of(false, false, "/man1/zshcompctl.1", "zshc", true, 6, 10, new int[] { 6, 7, 8, 9 },
SCORE_MATCH * 4 + BONUS_BOUNDARY_DELIMITER * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_DELIMITER * 3),
Arguments.of(false, false, "/.oh-my-zsh/cache", "zshc", true, 8, 13, new int[] { 8, 9, 10 ,12 },
SCORE_MATCH * 4 + BONUS_BOUNDARY * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY * 2 + SCORE_GAP_START + BONUS_BOUNDARY_DELIMITER),
Arguments.of(false, false, "ab0123 456", "12356", true, 3, 10, new int[] { 3, 4, 5, 8, 9 },
SCORE_MATCH * 5 + BONUS_CONSECUTIVE * 3 + SCORE_GAP_START + SCORE_GAP_EXTENSION),
Arguments.of(false, false, "abc123 456", "12356", true, 3, 10, new int[] { 3, 4, 5, 8, 9 },
SCORE_MATCH * 5 + BONUS_CAMEL123 * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 + BONUS_CONSECUTIVE + SCORE_GAP_START + SCORE_GAP_EXTENSION),
Arguments.of(false, false, "foo/bar/baz", "fbb", true, 0, 9, new int[] { 0, 4, 8 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_DELIMITER * 2 + SCORE_GAP_START * 2 + SCORE_GAP_EXTENSION * 4),
Arguments.of(false, false, "fooBarBaz", "fbb", true, 0, 7, new int[] { 0, 3, 6 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 + SCORE_GAP_START * 2 + SCORE_GAP_EXTENSION * 2),
Arguments.of(false, false, "foo barbaz", "fbb", true, 0, 8, new int[] { 0, 4, 7 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE +SCORE_GAP_START * 2 + SCORE_GAP_EXTENSION * 3),
Arguments.of(false, false, "fooBar Baz", "foob", true, 0, 4, new int[] { 0, 1, 2, 3 },
SCORE_MATCH * 4 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE * 3),
Arguments.of(false, false, "xFoo-Bar Baz", "foo-b", true, 1, 6, new int[] { 1, 2, 3, 4, 5 },
SCORE_MATCH * 5 + BONUS_CAMEL123 * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 + BONUS_NON_WORD + BONUS_BOUNDARY),
Arguments.of(true, false, "fooBarbaz", "oBz", true, 2, 9, new int[] { 2, 3, 8 },
SCORE_MATCH * 3 + BONUS_CAMEL123 + SCORE_GAP_START + SCORE_GAP_EXTENSION * 3),
Arguments.of(true, false, "Foo/Bar/Baz", "FBB", true, 0, 9, new int[] { 0, 4, 8 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_DELIMITER * 2 + SCORE_GAP_START * 2 + SCORE_GAP_EXTENSION * 4),
Arguments.of(true, false, "FooBarBaz", "FBB", true, 0, 7, new int[] { 0, 3, 6 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 + SCORE_GAP_START * 2 + SCORE_GAP_EXTENSION * 2),
Arguments.of(true, false, "FooBar Baz", "FooB", true, 0, 4, new int[] { 0, 1, 2, 3 },
SCORE_MATCH * 4 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE * 2 + Math.max(BONUS_CAMEL123, BONUS_BOUNDARY_WHITE)),
Arguments.of(true, false, "foo-bar", "o-ba", true, 2, 6, new int[] { 2, 3, 4, 5 },
SCORE_MATCH * 4 + BONUS_BOUNDARY * 3),
Arguments.of(true, false, "fooBarbaz", "oBZ", true, -1, -1, new int[0],
0),
Arguments.of(true, false, "Foo Bar Baz", "fbb", true, -1, -1, new int[0],
0),
Arguments.of(true, false, "fooBarbaz", "fooBarbazz", true, -1, -1, new int[0],
0)
);
}
@ParameterizedTest
@MethodSource
void testFuzzyMatchV1(boolean caseSensitive, boolean normalize, String text, String pattern, boolean withPos,
int start, int end, int[] positions, int score) {
if (!caseSensitive) {
pattern = pattern.toLowerCase();
}
SearchMatchResult result;
result = searchMatch.match(caseSensitive, normalize, true, text, pattern);
assertThat(result.getStart()).isEqualTo(start);
assertThat(result.getEnd()).isEqualTo(end);
assertThat(result.getScore()).isEqualTo(score);
assertThat(result.getPositions()).containsExactly(positions);
result = searchMatch.match(caseSensitive, normalize, false, text, pattern);
assertThat(result.getStart()).isEqualTo(start);
assertThat(result.getEnd()).isEqualTo(end);
assertThat(result.getScore()).isEqualTo(score);
assertThat(result.getPositions()).containsExactly(positions);
}
static Stream<Arguments> testFuzzyMatchV1Backward() {
return Stream.of(
Arguments.of(false, true, "foobar fb", "fb", 0, 4, new int[] { 0, 3 },
SCORE_MATCH * 2 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + SCORE_GAP_START + SCORE_GAP_EXTENSION),
Arguments.of(false, false, "foobar fb", "fb", 7, 9, new int[] { 7, 8 },
SCORE_MATCH * 2 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE)
);
}
@ParameterizedTest
@MethodSource
void testFuzzyMatchV1Backward(boolean caseSensitive, boolean forward, String text, String pattern, int start,
int end, int[] positions, int score) {
if (!caseSensitive) {
pattern = pattern.toLowerCase();
}
SearchMatchResult result;
result = searchMatch.match(caseSensitive, false, forward, text, pattern);
assertThat(result.getStart()).isEqualTo(start);
assertThat(result.getEnd()).isEqualTo(end);
assertThat(result.getScore()).isEqualTo(score);
assertThat(result.getPositions()).containsExactly(positions);
}
}

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_BOUNDARY;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_BOUNDARY_DELIMITER;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_BOUNDARY_WHITE;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_CAMEL123;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_CONSECUTIVE;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_FIRST_CHAR_MULTIPLIER;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.BONUS_NON_WORD;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.SCORE_GAP_EXTENSION;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.SCORE_GAP_START;
import static org.springframework.shell.support.search.AbstractSearchMatchAlgorithm.SCORE_MATCH;
class FuzzyMatchV2SearchMatchAlgorithmTests {
private final FuzzyMatchV2SearchMatchAlgorithm searchMatch = new FuzzyMatchV2SearchMatchAlgorithm();
static Stream<Arguments> testFuzzyMatchV2() {
return Stream.of(
Arguments.of(false, false, "fooBarbaz1", "oBZ", true, 2, 9, new int[] { 2, 3, 8 },
SCORE_MATCH * 3 + BONUS_CAMEL123 + SCORE_GAP_START + SCORE_GAP_EXTENSION * 3),
Arguments.of(false, false, "foo bar baz", "fbb", true, 0, 9, new int[] { 0, 4, 8 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE * 2 +
2 * SCORE_GAP_START + 4 * SCORE_GAP_EXTENSION),
Arguments.of(false, false, "/AutomatorDocument.icns", "rdoc", true, 9, 13, new int[] { 9, 10, 11, 12 },
SCORE_MATCH * 4 + BONUS_CAMEL123 + BONUS_CONSECUTIVE * 2),
Arguments.of(false, false, "/man1/zshcompctl.1", "zshc", true, 6, 10, new int[] { 6, 7, 8, 9 },
SCORE_MATCH * 4 + BONUS_BOUNDARY_DELIMITER * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_DELIMITER * 3),
Arguments.of(false, false, "/.oh-my-zsh/cache", "zshc", true, 8, 13, new int[] { 8, 9, 10 ,12 },
SCORE_MATCH * 4 + BONUS_BOUNDARY * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY * 2 + SCORE_GAP_START + BONUS_BOUNDARY_DELIMITER),
Arguments.of(false, false, "ab0123 456", "12356", true, 3, 10, new int[] { 3, 4, 5, 8, 9 },
SCORE_MATCH * 5 + BONUS_CONSECUTIVE * 3 + SCORE_GAP_START + SCORE_GAP_EXTENSION),
Arguments.of(false, false, "abc123 456", "12356", true, 3, 10, new int[] { 3, 4, 5, 8, 9 },
SCORE_MATCH * 5 + BONUS_CAMEL123 * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 + BONUS_CONSECUTIVE + SCORE_GAP_START + SCORE_GAP_EXTENSION),
Arguments.of(false, false, "foo/bar/baz", "fbb", true, 0, 9, new int[] { 0, 4, 8 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_DELIMITER * 2 + SCORE_GAP_START * 2 + SCORE_GAP_EXTENSION * 4),
Arguments.of(false, false, "fooBarBaz", "fbb", true, 0, 7, new int[] { 0, 3, 6 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 + SCORE_GAP_START * 2 + SCORE_GAP_EXTENSION * 2),
Arguments.of(false, false, "foo barbaz", "fbb", true, 0, 8, new int[] { 0, 4, 7 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE +SCORE_GAP_START * 2 + SCORE_GAP_EXTENSION * 3),
Arguments.of(false, false, "fooBar Baz", "foob", true, 0, 4, new int[] { 0, 1, 2, 3 },
SCORE_MATCH * 4 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE * 3),
Arguments.of(false, false, "xFoo-Bar Baz", "foo-b", true, 1, 6, new int[] { 1, 2, 3, 4, 5 },
SCORE_MATCH * 5 + BONUS_CAMEL123 * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 + BONUS_NON_WORD + BONUS_BOUNDARY),
Arguments.of(true, false, "fooBarbaz", "oBz", true, 2, 9, new int[] { 2, 3, 8 },
SCORE_MATCH * 3 + BONUS_CAMEL123 + SCORE_GAP_START + SCORE_GAP_EXTENSION * 3),
Arguments.of(true, false, "Foo/Bar/Baz", "FBB", true, 0, 9, new int[] { 0, 4, 8 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_DELIMITER * 2 + SCORE_GAP_START * 2 + SCORE_GAP_EXTENSION * 4),
Arguments.of(true, false, "FooBarBaz", "FBB", true, 0, 7, new int[] { 0, 3, 6 },
SCORE_MATCH * 3 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_CAMEL123 * 2 + SCORE_GAP_START * 2 + SCORE_GAP_EXTENSION * 2),
Arguments.of(true, false, "FooBar Baz", "FooB", true, 0, 4, new int[] { 0, 1, 2, 3 },
SCORE_MATCH * 4 + BONUS_BOUNDARY_WHITE * BONUS_FIRST_CHAR_MULTIPLIER + BONUS_BOUNDARY_WHITE * 2 + Math.max(BONUS_CAMEL123, BONUS_BOUNDARY_WHITE)),
Arguments.of(true, false, "foo-bar", "o-ba", true, 2, 6, new int[] { 2, 3, 4, 5 },
SCORE_MATCH * 4 + BONUS_BOUNDARY * 3),
Arguments.of(true, false, "fooBarbaz", "oBZ", true, -1, -1, new int[0],
0),
Arguments.of(true, false, "Foo Bar Baz", "fbb", true, -1, -1, new int[0],
0),
Arguments.of(true, false, "fooBarbaz", "fooBarbazz", true, -1, -1, new int[0],
0)
);
}
@ParameterizedTest
@MethodSource
void testFuzzyMatchV2(boolean caseSensitive, boolean normalize, String text, String pattern, boolean withPos,
int start, int end, int[] positions, int score) {
if (!caseSensitive) {
pattern = pattern.toLowerCase();
}
SearchMatchResult result;
result = searchMatch.match(caseSensitive, normalize, true, text, pattern);
assertThat(result.getStart()).isEqualTo(start);
assertThat(result.getEnd()).isEqualTo(end);
assertThat(result.getScore()).isEqualTo(score);
assertThat(result.getPositions()).containsExactly(positions);
result = searchMatch.match(caseSensitive, normalize, false, text, pattern);
assertThat(result.getStart()).isEqualTo(start);
assertThat(result.getEnd()).isEqualTo(end);
assertThat(result.getScore()).isEqualTo(score);
assertThat(result.getPositions()).containsExactly(positions);
}
static Stream<Arguments> testFuzzyMatchV2Normalize() {
return Stream.of(
Arguments.of(false, true, "Só Danço Samba", "So", false, 0, 2, new int[] { 0, 1 },
62),
Arguments.of(false, true, "Só Danço Samba", "sodc", false, 0, 7, new int[] { 0, 1, 3, 6 },
97),
Arguments.of(false, false, "So Danco Samba", "sodc", false, 0, 7, new int[] { 0, 1, 3, 6 },
97),
Arguments.of(false, true, "Danço", "danco", false, 0, 5, new int[] { 0, 1, 2, 3, 4 },
140)
);
}
@ParameterizedTest
@MethodSource
void testFuzzyMatchV2Normalize(boolean caseSensitive, boolean normalize, String text, String pattern, boolean withPos,
int start, int end, int[] positions, int score) {
if (!caseSensitive) {
pattern = pattern.toLowerCase();
}
if (normalize) {
pattern = Normalize.normalizeRunes(pattern);
}
SearchMatchResult result;
result = searchMatch.match(caseSensitive, normalize, true, text, pattern);
assertThat(result.getStart()).isEqualTo(start);
assertThat(result.getEnd()).isEqualTo(end);
assertThat(result.getScore()).isEqualTo(score);
assertThat(result.getPositions()).containsExactly(positions);
}
static Stream<Arguments> testFuzzyMatchV2Extra() {
return Stream.of(
Arguments.of(false, true, "/tmp/test/suomi/o.txt", "oo", false, 12, 17, new int[] { 12, 16 },
SCORE_MATCH * 2 + BONUS_BOUNDARY_DELIMITER + SCORE_GAP_START + SCORE_GAP_EXTENSION * 2),
Arguments.of(false, true, "/tmp/test/suomi/ö.txt", "oo", false, 12, 17, new int[] { 12, 16 },
SCORE_MATCH * 2 + BONUS_BOUNDARY_DELIMITER + SCORE_GAP_START + SCORE_GAP_EXTENSION * 2),
Arguments.of(false, true, "/tmp/test/suomi/O.txt", "oo", false, 12, 17, new int[] { 12, 16 },
SCORE_MATCH * 2 + BONUS_BOUNDARY_DELIMITER + SCORE_GAP_START + SCORE_GAP_EXTENSION * 2),
Arguments.of(false, true, "/tmp/test/suomi/Ö.txt", "oo", false, 12, 17, new int[] { 12, 16 },
SCORE_MATCH * 2 + BONUS_BOUNDARY_DELIMITER + SCORE_GAP_START + SCORE_GAP_EXTENSION * 2)
);
}
@ParameterizedTest
@MethodSource
void testFuzzyMatchV2Extra(boolean caseSensitive, boolean normalize, String text, String pattern, boolean withPos,
int start, int end, int[] positions, int score) {
if (!caseSensitive) {
pattern = pattern.toLowerCase();
}
if (normalize) {
pattern = Normalize.normalizeRunes(pattern);
}
SearchMatchResult result;
result = searchMatch.match(caseSensitive, normalize, true, text, pattern);
assertThat(result.getStart()).isEqualTo(start);
assertThat(result.getEnd()).isEqualTo(end);
assertThat(result.getScore()).isEqualTo(score);
assertThat(result.getPositions()).containsExactly(positions);
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class NormalizeTests {
@Test
void testNormalizeMap() {
assertThat(Normalize.normalized.get('á')).isEqualTo('a');
assertThat(Normalize.normalized.get('ự')).isEqualTo('u');
}
@Test
void testNormalizeFunction() {
assertThat(Normalize.normalizeRunes("abcáự")).isEqualTo("abcau");
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.support.search;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class SearchMatchTests {
@Test
void testAlgoType() {
SearchMatch searchMatch = SearchMatch.builder()
.caseSensitive(true)
.normalize(true)
.forward(true)
.build();
assertThat(searchMatch).isNotNull();
SearchMatchResult result = null;
result = searchMatch.match("fake", "fake");
assertThat(result.getAlgorithm()).isInstanceOf(FuzzyMatchV2SearchMatchAlgorithm.class);
result = searchMatch.match("fake", "'fake");
assertThat(result.getAlgorithm()).isInstanceOf(ExactMatchNaiveSearchMatchAlgorithm.class);
}
}

View File

@@ -15,6 +15,7 @@ dependencies {
api "org.jline:jline-terminal-jna:$jlineVersion"
api "org.jline:jline-terminal-jansi:$jlineVersion"
api "org.antlr:ST4:$st4Version"
api "commons-io:commons-io:$commonsIoVersion"
api "com.google.jimfs:jimfs:$jimfsVersion"
api "com.google.code.findbugs:jsr305:$findbugsVersion"
}

View File

@@ -30,7 +30,10 @@ import org.springframework.shell.component.ConfirmationInput.ConfirmationInputCo
import org.springframework.shell.component.MultiItemSelector;
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
import org.springframework.shell.component.PathInput;
import org.springframework.shell.component.PathSearch;
import org.springframework.shell.component.PathInput.PathInputContext;
import org.springframework.shell.component.PathSearch.PathSearchConfig;
import org.springframework.shell.component.PathSearch.PathSearchContext;
import org.springframework.shell.component.SingleItemSelector;
import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext;
import org.springframework.shell.component.StringInput;
@@ -57,7 +60,7 @@ public class ComponentCommands extends AbstractShellComponent {
return "Got value " + context.getResultValue();
}
@ShellMethod(key = "component path", value = "Path input", group = "Components")
@ShellMethod(key = "component path input", value = "Path input", group = "Components")
public String pathInput() {
PathInput component = new PathInput(getTerminal(), "Enter value");
component.setResourceLoader(getResourceLoader());
@@ -66,6 +69,31 @@ public class ComponentCommands extends AbstractShellComponent {
return "Got value " + context.getResultValue();
}
@ShellMethod(key = "component path search", value = "Path search", group = "Components")
public String pathSearch(
@ShellOption(defaultValue = ShellOption.NULL) Integer maxPathsShow,
@ShellOption(defaultValue = ShellOption.NULL) Integer maxPathsSearch,
@ShellOption(defaultValue = "true") boolean searchForward,
@ShellOption(defaultValue = "false") boolean searchCaseSensitive,
@ShellOption(defaultValue = "false") boolean searchNormalize
) {
PathSearchConfig config = new PathSearch.PathSearchConfig();
if (maxPathsShow != null) {
config.setMaxPathsShow(maxPathsShow);
}
if (maxPathsSearch != null) {
config.setMaxPathsSearch(maxPathsSearch);
}
config.setSearchForward(searchForward);
config.setSearchCaseSensitive(searchCaseSensitive);
config.setSearchNormalize(searchNormalize);
PathSearch component = new PathSearch(getTerminal(), "Enter value", config);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
PathSearchContext context = component.run(PathSearchContext.empty());
return "Got value " + context.getResultValue();
}
@ShellMethod(key = "component confirmation", value = "Confirmation input", group = "Components")
public String confirmationInput(boolean no) {
ConfirmationInput component = new ConfirmationInput(getTerminal(), "Enter value", !no);