diff --git a/src/vs/base/common/scorer.ts b/src/vs/base/common/scorer.ts index 84d43f4e2a4..fd75831a31a 100644 --- a/src/vs/base/common/scorer.ts +++ b/src/vs/base/common/scorer.ts @@ -45,7 +45,7 @@ BEGIN THIRD PARTY * Start of word/path bonus: 7 * Start of string bonus: 8 */ -export function _doScore(target: string, query: string): Score { +export function _doScore(target: string, query: string, inverse?: boolean): Score { if (!target || !query) { return NO_SCORE; // return early if target or query are undefined } @@ -62,11 +62,25 @@ export function _doScore(target: string, query: string): Score { const matchingPositions: number[] = []; - let index = 0; - let startAt = 0; + let index: number; + let startAt: number; + if (!inverse) { + index = 0; + startAt = 0; + } else { + index = queryLen - 1; // inverse: from end of query to beginning + startAt = target.length - 1; // inverse: from end of target to beginning + } + let score = 0; - while (index < queryLen) { - let indexOf = targetLower.indexOf(queryLower[index], startAt); + while (inverse ? index >= 0 : index < queryLen) { + let indexOf: number; + if (!inverse) { + indexOf = targetLower.indexOf(queryLower[index], startAt); + } else { + indexOf = targetLower.lastIndexOf(queryLower[index], startAt); // inverse: look from the end + } + if (indexOf < 0) { // console.log(`Character not part of target ${query[index]}`); @@ -120,8 +134,18 @@ export function _doScore(target: string, query: string): Score { // console.groupEnd(); - startAt = indexOf + 1; - index++; + if (!inverse) { + startAt = indexOf + 1; + index++; + } else { + startAt = indexOf - 1; // inverse: go to begining from end + index--; // inverse: also for query index + } + } + + // inverse: flip the matching positions so that they appear in order + if (inverse) { + matchingPositions.reverse(); } const res: Score = (score > 0) ? [score, matchingPositions] : NO_SCORE; @@ -248,7 +272,18 @@ function doScoreItem(label: string, description: string, path: string, query: const descriptionPrefixLength = descriptionPrefix.length; const descriptionAndLabel = `${descriptionPrefix}${label}`; - const [labelDescriptionScore, labelDescriptionPositions] = _doScore(descriptionAndLabel, query); + let [labelDescriptionScore, labelDescriptionPositions] = _doScore(descriptionAndLabel, query); + + // Optimize for file paths: score from the back to the beginning to catch more specific folder + // names that match on the end of the file. This yields better results in most cases. + if (!!path) { + const [labelDescriptionScoreInverse, labelDescriptionPositionsInverse] = _doScore(descriptionAndLabel, query, true /* inverse */); + if (labelDescriptionScoreInverse && labelDescriptionScoreInverse > labelDescriptionScore) { + labelDescriptionScore = labelDescriptionScoreInverse; + labelDescriptionPositions = labelDescriptionPositionsInverse; + } + } + if (labelDescriptionScore) { const labelDescriptionMatches = createMatches(labelDescriptionPositions); const labelMatch: IMatch[] = []; diff --git a/src/vs/base/test/common/scorer.test.ts b/src/vs/base/test/common/scorer.test.ts index a7a3d69a3a2..e6d4de7bbdf 100644 --- a/src/vs/base/test/common/scorer.test.ts +++ b/src/vs/base/test/common/scorer.test.ts @@ -79,7 +79,7 @@ suite('Scorer', () => { assert.equal(positions[1], 6); }); - test('scoreFile - matches are proper', function () { + test('scoreItem - matches are proper', function () { let res = scorer.scoreItem(null, 'something', ResourceAccessor, cache); assert.ok(!res.score); @@ -151,7 +151,25 @@ suite('Scorer', () => { assert.ok(pathRes.score > noRes.score); }); - test('compareFilesByScore - identity', function () { + test('scoreItem - optimize for file paths', function () { + const resource = URI.file('/xyz/others/spath/some/xsp/file123.txt'); + + // xsp is more relevant to the end of the file path even though it matches + // fuzzy also in the beginning. we verify the more relevant match at the + // end gets returned. + const pathRes = scorer.scoreItem(resource, 'xspfile123', ResourceAccessor, cache); + assert.ok(pathRes.score); + assert.ok(pathRes.descriptionMatch); + assert.ok(pathRes.labelMatch); + assert.equal(pathRes.labelMatch.length, 1); + assert.equal(pathRes.labelMatch[0].start, 0); + assert.equal(pathRes.labelMatch[0].end, 7); + assert.equal(pathRes.descriptionMatch.length, 1); + assert.equal(pathRes.descriptionMatch[0].start, 23); + assert.equal(pathRes.descriptionMatch[0].end, 26); + }); + + test('compareItemsByScore - identity', function () { const resourceA = URI.file('/some/path/fileA.txt'); const resourceB = URI.file('/some/path/other/fileB.txt'); const resourceC = URI.file('/unrelated/some/path/other/fileC.txt');