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:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
>>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user