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:
parent
ad42148eeb
commit
c574731ae9
|
@ -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 } {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue