Break ties with second pass

This commit is contained in:
Clément Fournier
2019-01-11 07:55:13 +01:00
parent 0e442da8f8
commit a7c79d2683
3 changed files with 50 additions and 11 deletions

View File

@@ -177,8 +177,7 @@ public class XPathPanelController implements Initializable, SettingsOwner {
XPathSuggestionMaker suggestionMaker = XPathSuggestionMaker.forLanguage(parent.getLanguageVersion().getLanguage());
List<MenuItem> suggestions =
suggestionMaker.getSortedMatches(input)
.limit(5)
suggestionMaker.getSortedMatches(input, 5)
.map(result -> {
Label entryLabel = new Label();

View File

@@ -29,19 +29,43 @@ class ResultSelectionStrategy {
private static final int MIN_QUERY_LENGTH = 1;
Stream<MatchResult> filterResults(List<String> candidates, String query) {
Stream<MatchResult> filterResults(List<String> candidates, String query, int limit) {
if (query.length() < MIN_QUERY_LENGTH) {
return Stream.empty();
}
return candidates.stream()
.map(cand -> computeMatchingSegments(cand, query))
.map(cand -> computeMatchingSegments(cand, query, false))
.sorted(Comparator.comparingInt(MatchResult::getScore).reversed())
.limit(15);
// second pass is done only on those we know we'll keep
.limit(limit)
.map(prev -> {
// try to break ties between the top results, e.g.
//
// without second pass, we have a tie:
// query coit
// candidate ClassOrInterfaceType : 32
// candidate ClassOrInterfaceBodyDeclaration : 32
// ^ ^ ^ ^
// with second pass:
//
// query coit
// candidate ClassOrInterfaceType : 40 -> and indeed it's the best match
// ^ ^ ^ ^
// candidate ClassOrInterfaceDeclaration : 32
// ^ ^ ^ ^
MatchResult refined = computeMatchingSegments(prev.getNodeName(), query, true);
// keep the best
return refined.getScore() > prev.getScore() ? refined : prev;
})
.sorted(Comparator.comparingInt(MatchResult::getScore).reversed());
}
private Text makeHighlightedText(String match) {
Text matchLabel = new Text(match);
matchLabel.getStyleClass().add("autocomplete-match");
@@ -49,9 +73,15 @@ class ResultSelectionStrategy {
}
// ok it's ugly and is not relevant in all cases
// but given that we don't have many candidates to choose from I think it works great
private MatchResult computeMatchingSegments(String candidate, String query) {
/**
* Computes a match result with its score for the candidate and query.
*
* @param candidate Candidate string
* @param query Query
* @param matchOnlyWordStarts Whether to only match word starts. This is a more unfair strategy
* that can be used to break ties.
*/
private MatchResult computeMatchingSegments(String candidate, String query, boolean matchOnlyWordStarts) {
int candIdx = 0;
int queryIdx = 0;
@@ -62,6 +92,7 @@ class ResultSelectionStrategy {
// length of the continuous match
int matchLength = 0;
// whether the current match is the start of a camelcase word
boolean isStartOfWord = true;
TextFlow flow = new TextFlow();
@@ -78,6 +109,14 @@ class ResultSelectionStrategy {
if (curMatchStart == -1) {
// start of a match
if (matchOnlyWordStarts && !isStartOfWord && !Character.isUpperCase(candChar)) {
// not the start of a word, don't record it as a match
candIdx++;
continue;
}
// set match start to current
curMatchStart = candIdx;
if (Character.isUpperCase(candChar)) {
@@ -121,7 +160,6 @@ class ResultSelectionStrategy {
candIdx++;
queryIdx++;
} else {
// the current chars don't match
@@ -148,6 +186,7 @@ class ResultSelectionStrategy {
// reset match
curMatchStart = -1;
matchLength = 0;
isStartOfWord = false;
}
}

View File

@@ -28,9 +28,10 @@ public final class XPathSuggestionMaker {
/**
* Returns a stream of pre-built TextFlows sorted by relevance.
* The stream will contain at most "limit" elements.
*/
public Stream<MatchResult> getSortedMatches(String input) {
return mySelectionStrategy.filterResults(myNameFinder.getNodeNames(), input);
public Stream<MatchResult> getSortedMatches(String input, int limit) {
return mySelectionStrategy.filterResults(myNameFinder.getNodeNames(), input, limit);
}
/**