Fix non-fuzzy highlighting in quick input (#131292)

* initial implementation that highlights consecutive maches correctly

* fuzzy -> allowNonContiguousMatches

* alternative implementation

* add tests and rename property to align

* use lower strings
This commit is contained in:
Tyler James Leonhardt 2021-08-23 10:59:04 -07:00 committed by GitHub
parent ad42148eeb
commit c574731ae9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 57 additions and 43 deletions

View file

@ -21,7 +21,7 @@ const NO_SCORE: FuzzyScore = [NO_MATCH, []];
// const DEBUG = false;
// const DEBUG_MATRIX = false;
export function scoreFuzzy(target: string, query: string, queryLower: string, fuzzy: boolean): FuzzyScore {
export function scoreFuzzy(target: string, query: string, queryLower: string, allowNonContiguousMatches: boolean): FuzzyScore {
if (!target || !query) {
return NO_SCORE; // return early if target or query are undefined
}
@ -38,20 +38,7 @@ export function scoreFuzzy(target: string, query: string, queryLower: string, fu
// }
const targetLower = target.toLowerCase();
// When not searching fuzzy, we require the query to be contained fully
// in the target string contiguously.
if (!fuzzy) {
if (!targetLower.includes(queryLower)) {
// if (DEBUG) {
// console.log(`Characters not matching consecutively ${queryLower} within ${targetLower}`);
// }
return NO_SCORE;
}
}
const res = doScoreFuzzy(query, queryLower, queryLength, target, targetLower, targetLength);
const res = doScoreFuzzy(query, queryLower, queryLength, target, targetLower, targetLength, allowNonContiguousMatches);
// if (DEBUG) {
// console.log(`%cFinal Score: ${res[0]}`, 'font-weight: bold');
@ -61,7 +48,7 @@ export function scoreFuzzy(target: string, query: string, queryLower: string, fu
return res;
}
function doScoreFuzzy(query: string, queryLower: string, queryLength: number, target: string, targetLower: string, targetLength: number): FuzzyScore {
function doScoreFuzzy(query: string, queryLower: string, queryLength: number, target: string, targetLower: string, targetLength: number, allowNonContiguousMatches: boolean): FuzzyScore {
const scores: number[] = [];
const matches: number[] = [];
@ -116,7 +103,17 @@ function doScoreFuzzy(query: string, queryLower: string, queryLength: number, ta
// We have a score and its equal or larger than the left score
// Match: sequence continues growing from previous diag value
// Score: increases by diag score value
if (score && diagScore + score >= leftScore) {
const isValidScore = score && diagScore + score >= leftScore;
if (isValidScore && (
// We don't need to check if it's contiguous if we allow non-contiguous matches
allowNonContiguousMatches ||
// We must be looking for a contiguous match.
// Looking at an index higher than 0 in the query means we must have already
// found out this is contiguous otherwise there wouldn't have been a score
queryIndexGtNull ||
// lastly check if the query is completely contiguous at this index in the target
targetLower.startsWith(queryLower, targetIndex)
)) {
matches[currentIndex] = matchesSequenceLength + 1;
scores[currentIndex] = diagScore + score;
}
@ -372,7 +369,7 @@ const PATH_IDENTITY_SCORE = 1 << 18;
const LABEL_PREFIX_SCORE_THRESHOLD = 1 << 17;
const LABEL_SCORE_THRESHOLD = 1 << 16;
export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor<T>, cache: FuzzyScorerCache): IItemScore {
export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, allowNonContiguousMatches: boolean, accessor: IItemAccessor<T>, cache: FuzzyScorerCache): IItemScore {
if (!item || !query.normalized) {
return NO_ITEM_SCORE; // we need an item and query to score on at least
}
@ -392,9 +389,9 @@ export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, fuzzy: boolean
// - whether fuzzy matching is enabled or not
let cacheHash: string;
if (description) {
cacheHash = `${label}${description}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}${query.expectExactMatch}`;
cacheHash = `${label}${description}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${allowNonContiguousMatches}${query.expectContiguousMatch}`;
} else {
cacheHash = `${label}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}${query.expectExactMatch}`;
cacheHash = `${label}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${allowNonContiguousMatches}${query.expectContiguousMatch}`;
}
const cached = cache[cacheHash];
@ -402,13 +399,13 @@ export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, fuzzy: boolean
return cached;
}
const itemScore = doScoreItemFuzzy(label, description, accessor.getItemPath(item), query, fuzzy);
const itemScore = doScoreItemFuzzy(label, description, accessor.getItemPath(item), query, allowNonContiguousMatches);
cache[cacheHash] = itemScore;
return itemScore;
}
function doScoreItemFuzzy(label: string, description: string | undefined, path: string | undefined, query: IPreparedQuery, fuzzy: boolean): IItemScore {
function doScoreItemFuzzy(label: string, description: string | undefined, path: string | undefined, query: IPreparedQuery, allowNonContiguousMatches: boolean): IItemScore {
const preferLabelMatches = !path || !query.containsPathSeparator;
// Treat identity matches on full path highest
@ -418,20 +415,20 @@ function doScoreItemFuzzy(label: string, description: string | undefined, path:
// Score: multiple inputs
if (query.values && query.values.length > 1) {
return doScoreItemFuzzyMultiple(label, description, path, query.values, preferLabelMatches, fuzzy);
return doScoreItemFuzzyMultiple(label, description, path, query.values, preferLabelMatches, allowNonContiguousMatches);
}
// Score: single input
return doScoreItemFuzzySingle(label, description, path, query, preferLabelMatches, fuzzy);
return doScoreItemFuzzySingle(label, description, path, query, preferLabelMatches, allowNonContiguousMatches);
}
function doScoreItemFuzzyMultiple(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece[], preferLabelMatches: boolean, fuzzy: boolean): IItemScore {
function doScoreItemFuzzyMultiple(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece[], preferLabelMatches: boolean, allowNonContiguousMatches: boolean): IItemScore {
let totalScore = 0;
const totalLabelMatches: IMatch[] = [];
const totalDescriptionMatches: IMatch[] = [];
for (const queryPiece of query) {
const { score, labelMatch, descriptionMatch } = doScoreItemFuzzySingle(label, description, path, queryPiece, preferLabelMatches, fuzzy);
const { score, labelMatch, descriptionMatch } = doScoreItemFuzzySingle(label, description, path, queryPiece, preferLabelMatches, allowNonContiguousMatches);
if (score === NO_MATCH) {
// if a single query value does not match, return with
// no score entirely, we require all queries to match
@ -457,7 +454,7 @@ function doScoreItemFuzzyMultiple(label: string, description: string | undefined
};
}
function doScoreItemFuzzySingle(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece, preferLabelMatches: boolean, fuzzy: boolean): IItemScore {
function doScoreItemFuzzySingle(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece, preferLabelMatches: boolean, allowNonContiguousMatches: boolean): IItemScore {
// Prefer label matches if told so or we have no description
if (preferLabelMatches || !description) {
@ -465,7 +462,7 @@ function doScoreItemFuzzySingle(label: string, description: string | undefined,
label,
query.normalized,
query.normalizedLowercase,
fuzzy && !query.expectExactMatch);
allowNonContiguousMatches && !query.expectContiguousMatch);
if (labelScore) {
// If we have a prefix match on the label, we give a much
@ -507,7 +504,7 @@ function doScoreItemFuzzySingle(label: string, description: string | undefined,
descriptionAndLabel,
query.normalized,
query.normalizedLowercase,
fuzzy && !query.expectExactMatch);
allowNonContiguousMatches && !query.expectContiguousMatch);
if (labelDescriptionScore) {
const labelDescriptionMatches = createMatches(labelDescriptionPositions);
const labelMatch: IMatch[] = [];
@ -606,9 +603,9 @@ function matchOverlaps(matchA: IMatch, matchB: IMatch): boolean {
//#region Comparers
export function compareItemsByFuzzyScore<T>(itemA: T, itemB: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor<T>, cache: FuzzyScorerCache): number {
const itemScoreA = scoreItemFuzzy(itemA, query, fuzzy, accessor, cache);
const itemScoreB = scoreItemFuzzy(itemB, query, fuzzy, accessor, cache);
export function compareItemsByFuzzyScore<T>(itemA: T, itemB: T, query: IPreparedQuery, allowNonContiguousMatches: boolean, accessor: IItemAccessor<T>, cache: FuzzyScorerCache): number {
const itemScoreA = scoreItemFuzzy(itemA, query, allowNonContiguousMatches, accessor, cache);
const itemScoreB = scoreItemFuzzy(itemB, query, allowNonContiguousMatches, accessor, cache);
const scoreA = itemScoreA.score;
const scoreB = itemScoreB.score;
@ -807,7 +804,7 @@ export interface IPreparedQueryPiece {
* this query must be a substring of the input.
* In other words, no fuzzy matching is used.
*/
expectExactMatch: boolean;
expectContiguousMatch: boolean;
}
export interface IPreparedQuery extends IPreparedQueryPiece {
@ -869,13 +866,13 @@ export function prepareQuery(original: string): IPreparedQuery {
pathNormalized: pathNormalizedPiece,
normalized: normalizedPiece,
normalizedLowercase: normalizedLowercasePiece,
expectExactMatch: expectExactMatchPiece
expectContiguousMatch: expectExactMatchPiece
});
}
}
}
return { original, originalLowercase, pathNormalized, normalized, normalizedLowercase, values, containsPathSeparator, expectExactMatch };
return { original, originalLowercase, pathNormalized, normalized, normalizedLowercase, values, containsPathSeparator, expectContiguousMatch: expectExactMatch };
}
function normalizeQuery(original: string): { pathNormalized: string, normalized: string, normalizedLowercase: string } {

View file

@ -76,10 +76,10 @@ class NullAccessorClass implements IItemAccessor<URI> {
}
}
function _doScore(target: string, query: string, fuzzy: boolean): FuzzyScore {
function _doScore(target: string, query: string, allowNonContiguousMatches?: boolean): FuzzyScore {
const preparedQuery = prepareQuery(query);
return scoreFuzzy(target, preparedQuery.normalized, preparedQuery.normalizedLowercase, fuzzy);
return scoreFuzzy(target, preparedQuery.normalized, preparedQuery.normalizedLowercase, allowNonContiguousMatches ?? !preparedQuery.expectContiguousMatch);
}
function _doScore2(target: string, query: string, matchOffset: number = 0): FuzzyScore2 {
@ -88,12 +88,12 @@ function _doScore2(target: string, query: string, matchOffset: number = 0): Fuzz
return scoreFuzzy2(target, preparedQuery, 0, matchOffset);
}
function scoreItem<T>(item: T, query: string, fuzzy: boolean, accessor: IItemAccessor<T>): IItemScore {
return scoreItemFuzzy(item, prepareQuery(query), fuzzy, accessor, Object.create(null));
function scoreItem<T>(item: T, query: string, allowNonContiguousMatches: boolean, accessor: IItemAccessor<T>): IItemScore {
return scoreItemFuzzy(item, prepareQuery(query), allowNonContiguousMatches, accessor, Object.create(null));
}
function compareItemsByScore<T>(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: IItemAccessor<T>): number {
return compareItemsByFuzzyScore(itemA, itemB, prepareQuery(query), fuzzy, accessor, Object.create(null));
function compareItemsByScore<T>(itemA: T, itemB: T, query: string, allowNonContiguousMatches: boolean, accessor: IItemAccessor<T>): number {
return compareItemsByFuzzyScore(itemA, itemB, prepareQuery(query), allowNonContiguousMatches, accessor, Object.create(null));
}
const NullAccessor = new NullAccessorClass();
@ -1082,11 +1082,11 @@ suite('Fuzzy Scorer', () => {
assert.strictEqual(prepareQuery('model Tester.ts').original, 'model Tester.ts');
assert.strictEqual(prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase());
assert.strictEqual(prepareQuery('model Tester.ts').normalized, 'modelTester.ts');
assert.strictEqual(prepareQuery('model Tester.ts').expectExactMatch, false); // doesn't have quotes in it
assert.strictEqual(prepareQuery('model Tester.ts').expectContiguousMatch, false); // doesn't have quotes in it
assert.strictEqual(prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts');
assert.strictEqual(prepareQuery('ModelTester.ts').containsPathSeparator, false);
assert.strictEqual(prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true);
assert.strictEqual(prepareQuery('"hello"').expectExactMatch, true);
assert.strictEqual(prepareQuery('"hello"').expectContiguousMatch, true);
assert.strictEqual(prepareQuery('"hello"').normalized, 'hello');
// with spaces
@ -1215,4 +1215,21 @@ suite('Fuzzy Scorer', () => {
assert.ok(typeof score[0] === 'number');
assert.ok(score[1].length > 0);
});
test('Using quotes should expect contiguous matches match', function () {
// missing the "i" in the query
assert.strictEqual(_doScore('contiguous', '"contguous"')[0], 0);
const score = _doScore('contiguous', '"contiguous"');
assert.strictEqual(score[0], 253);
});
test('Using quotes should highlight contiguous indexes', function () {
const score = _doScore('2021-7-26.md', '"26"');
assert.strictEqual(score[0], 13);
// The indexes of the 2 and 6 of "26"
assert.strictEqual(score[1][0], 7);
assert.strictEqual(score[1][1], 8);
});
});