From 7a2b6fd7208b80e5f76cbd7797a180ea948b2b1f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 4 Oct 2017 10:44:35 +0200 Subject: [PATCH] more predictable recent files sorting (#10690, #20546) --- src/vs/base/common/OSSREADME.json | 2 +- src/vs/base/common/filters.ts | 4 +- .../parts/quickopen/browser/quickOpenModel.ts | 145 +++++------------- .../quickopen/common/quickOpenScorer.ts} | 64 +++++--- .../test/common/quickOpenScorer.test.ts} | 128 ++++++++++------ .../browser/parts/editor/editorPicker.ts | 6 +- .../parts/quickopen/quickOpenController.ts | 83 +++++----- .../search/browser/openAnythingHandler.ts | 6 +- .../parts/search/browser/openSymbolHandler.ts | 4 +- .../services/search/node/rawSearchService.ts | 4 +- 10 files changed, 216 insertions(+), 230 deletions(-) rename src/vs/base/{common/scorer.ts => parts/quickopen/common/quickOpenScorer.ts} (85%) rename src/vs/base/{test/common/scorer.test.ts => parts/quickopen/test/common/quickOpenScorer.test.ts} (71%) diff --git a/src/vs/base/common/OSSREADME.json b/src/vs/base/common/OSSREADME.json index aafe65aace5..e6948b8a57b 100644 --- a/src/vs/base/common/OSSREADME.json +++ b/src/vs/base/common/OSSREADME.json @@ -4,7 +4,7 @@ "version": "0.1.20", "license": "MIT", "repositoryURL": "https://github.com/joshaven/string_score", - "description": "The file scorer.ts was inspired by the string_score algorithm from Joshaven Potter.", + "description": "The file quickOpenScorer.ts was inspired by the string_score algorithm from Joshaven Potter.", "licenseDetail": [ "This software is released under the MIT license:", "", diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index e832af396c7..a943fc15c03 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -122,7 +122,7 @@ function isLower(code: number): boolean { return CharCode.a <= code && code <= CharCode.z; } -function isUpper(code: number): boolean { +export function isUpper(code: number): boolean { return CharCode.A <= code && code <= CharCode.Z; } @@ -421,7 +421,7 @@ function printTable(table: number[][], pattern: string, patternLen: number, word return ret; } -function isSeparatorAtPos(value: string, index: number): boolean { +export function isSeparatorAtPos(value: string, index: number): boolean { if (index < 0 || index >= value.length) { return false; } diff --git a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts index bf000d343dd..773a4b79b4d 100644 --- a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts +++ b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts @@ -10,9 +10,6 @@ import { TPromise } from 'vs/base/common/winjs.base'; import types = require('vs/base/common/types'); import URI from 'vs/base/common/uri'; import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree'; -import filters = require('vs/base/common/filters'); -import strings = require('vs/base/common/strings'); -import paths = require('vs/base/common/paths'); import { IconLabel, IIconLabelOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IQuickNavigateConfiguration, IModel, IDataSource, IFilter, IAccessiblityProvider, IRenderer, IRunner, Mode } from 'vs/base/parts/quickopen/common/quickOpen'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; @@ -24,7 +21,7 @@ import { IQuickOpenStyles } from 'vs/base/parts/quickopen/browser/quickOpenWidge import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { OS } from 'vs/base/common/platform'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { IItemAccessor } from 'vs/base/common/scorer'; +import { IItemAccessor } from 'vs/base/parts/quickopen/common/quickOpenScorer'; export interface IContext { event: any; @@ -174,112 +171,6 @@ export class QuickOpenEntry { return false; } - /** - * A good default sort implementation for quick open entries respecting highlight information - * as well as associated resources. - */ - public static compare(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string): number { - - // Give matches with label highlights higher priority over - // those with only description highlights - const labelHighlightsA = elementA.getHighlights()[0] || []; - const labelHighlightsB = elementB.getHighlights()[0] || []; - if (labelHighlightsA.length && !labelHighlightsB.length) { - return -1; - } else if (!labelHighlightsA.length && labelHighlightsB.length) { - return 1; - } - - // Fallback to the full path if labels are identical and we have associated resources - let nameA = elementA.getLabel(); - let nameB = elementB.getLabel(); - if (nameA === nameB) { - const resourceA = elementA.getResource(); - const resourceB = elementB.getResource(); - - if (resourceA && resourceB) { - nameA = resourceA.fsPath; - nameB = resourceB.fsPath; - } - } - - return compareAnything(nameA, nameB, lookFor); - } - - /** - * A good default highlight implementation for an entry with label and description. - */ - public static highlight(entry: QuickOpenEntry, lookFor: string, fuzzyHighlight = false): { labelHighlights: IHighlight[], descriptionHighlights: IHighlight[] } { - let labelHighlights: IHighlight[] = []; - const descriptionHighlights: IHighlight[] = []; - - const normalizedLookFor = strings.stripWildcards(lookFor); - const label = entry.getLabel(); - const description = entry.getDescription(); - - // Highlight file aware - if (entry.getResource()) { - - // Highlight entire label and description if searching for full absolute path - const fsPath = entry.getResource().fsPath; - if (lookFor.length === fsPath.length && lookFor.toLowerCase() === fsPath.toLowerCase()) { - labelHighlights.push({ start: 0, end: label.length }); - descriptionHighlights.push({ start: 0, end: description.length }); - } - - // Fuzzy/Full-Path: Highlight is special - else if (fuzzyHighlight || lookFor.indexOf(paths.nativeSep) >= 0) { - const candidateLabelHighlights = filters.matchesFuzzy(lookFor, label, fuzzyHighlight); - if (!candidateLabelHighlights) { - const pathPrefix = description ? (description + paths.nativeSep) : ''; - const pathPrefixLength = pathPrefix.length; - - // If there are no highlights in the label, build a path out of description and highlight and match on both, - // then extract the individual label and description highlights back to the original positions - let pathHighlights = filters.matchesFuzzy(lookFor, pathPrefix + label, fuzzyHighlight); - if (!pathHighlights && lookFor !== normalizedLookFor) { - pathHighlights = filters.matchesFuzzy(normalizedLookFor, pathPrefix + label, fuzzyHighlight); - } - - if (pathHighlights) { - pathHighlights.forEach(h => { - - // Match overlaps label and description part, we need to split it up - if (h.start < pathPrefixLength && h.end > pathPrefixLength) { - labelHighlights.push({ start: 0, end: h.end - pathPrefixLength }); - descriptionHighlights.push({ start: h.start, end: pathPrefixLength }); - } - - // Match on label part - else if (h.start >= pathPrefixLength) { - labelHighlights.push({ start: h.start - pathPrefixLength, end: h.end - pathPrefixLength }); - } - - // Match on description part - else { - descriptionHighlights.push(h); - } - }); - } - } else { - labelHighlights = candidateLabelHighlights; - } - } - - // Highlight only inside label - else { - labelHighlights = filters.matchesFuzzy(lookFor, label); - } - } - - // Highlight by label otherwise - else { - labelHighlights = filters.matchesFuzzy(lookFor, label); - } - - return { labelHighlights, descriptionHighlights }; - } - public isFile(): boolean { return false; // TODO@Ben debt with editor history merging } @@ -690,3 +581,37 @@ export class QuickOpenModel implements return entry.run(mode, context); } } + +/** + * A good default sort implementation for quick open entries respecting highlight information + * as well as associated resources. + */ +export function compareEntries(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string): number { + + // Give matches with label highlights higher priority over + // those with only description highlights + const labelHighlightsA = elementA.getHighlights()[0] || []; + const labelHighlightsB = elementB.getHighlights()[0] || []; + if (labelHighlightsA.length && !labelHighlightsB.length) { + return -1; + } + + if (!labelHighlightsA.length && labelHighlightsB.length) { + return 1; + } + + // Fallback to the full path if labels are identical and we have associated resources + let nameA = elementA.getLabel(); + let nameB = elementB.getLabel(); + if (nameA === nameB) { + const resourceA = elementA.getResource(); + const resourceB = elementB.getResource(); + + if (resourceA && resourceB) { + nameA = resourceA.fsPath; + nameB = resourceB.fsPath; + } + } + + return compareAnything(nameA, nameB, lookFor); +} \ No newline at end of file diff --git a/src/vs/base/common/scorer.ts b/src/vs/base/parts/quickopen/common/quickOpenScorer.ts similarity index 85% rename from src/vs/base/common/scorer.ts rename to src/vs/base/parts/quickopen/common/quickOpenScorer.ts index b3da8d89d24..981c6c830a7 100644 --- a/src/vs/base/common/scorer.ts +++ b/src/vs/base/parts/quickopen/common/quickOpenScorer.ts @@ -6,7 +6,7 @@ 'use strict'; import { compareAnything } from 'vs/base/common/comparers'; -import { matchesPrefix, IMatch, createMatches, matchesCamelCase } from 'vs/base/common/filters'; +import { matchesPrefix, IMatch, createMatches, matchesCamelCase, isSeparatorAtPos, isUpper } from 'vs/base/common/filters'; import { isEqual, nativeSep } from 'vs/base/common/paths'; export type Score = [number /* score */, number[] /* match positions */]; @@ -14,8 +14,6 @@ export type ScorerCache = { [key: string]: IItemScore }; const NO_SCORE: Score = [0, []]; -const wordPathBoundary = ['-', '_', ' ', '/', '\\', '.']; - // Based on material from: /*! BEGIN THIRD PARTY @@ -45,7 +43,7 @@ BEGIN THIRD PARTY * Start of word/path bonus: 7 * Start of string bonus: 8 */ -export function _doScore(target: string, query: string, inverse?: boolean): Score { +export function _doScore(target: string, query: string, fuzzy: boolean, inverse?: boolean): Score { if (!target || !query) { return NO_SCORE; // return early if target or query are undefined } @@ -72,8 +70,34 @@ export function _doScore(target: string, query: string, inverse?: boolean): Scor startAt = target.length - 1; // inverse: from end of target to beginning } + // When not searching fuzzy, we require the query to be contained fully + // in the target string. + if (!fuzzy) { + let indexOfQueryInTarget: number; + if (!inverse) { + indexOfQueryInTarget = targetLower.indexOf(queryLower); + } else { + indexOfQueryInTarget = targetLower.lastIndexOf(queryLower); + } + + if (indexOfQueryInTarget === -1) { + // console.log(`Characters not matching consecutively ${queryLower} within ${targetLower}`); + + return NO_SCORE; + } + + // Adjust the start position with the offset of the query + if (!inverse) { + startAt = indexOfQueryInTarget; + } else { + startAt = indexOfQueryInTarget + query.length; + } + } + let score = 0; while (inverse ? index >= 0 : index < queryLen) { + + // Check for query character being contained in target let indexOf: number; if (!inverse) { indexOf = targetLower.indexOf(queryLower[index], startAt); @@ -82,10 +106,9 @@ export function _doScore(target: string, query: string, inverse?: boolean): Scor } if (indexOf < 0) { - // console.log(`Character not part of target ${query[index]}`); - score = 0; // This makes sure that the query is contained in the target + score = 0; break; } @@ -119,7 +142,7 @@ export function _doScore(target: string, query: string, inverse?: boolean): Scor } // After separator bonus - else if (wordPathBoundary.some(w => w === target[indexOf - 1])) { + else if (isSeparatorAtPos(target, indexOf - 1)) { score += 7; // console.log('After separtor bonus: +7'); @@ -156,9 +179,6 @@ export function _doScore(target: string, query: string, inverse?: boolean): Scor return res; } -function isUpper(code: number): boolean { - return 65 <= code && code <= 90; -} /*! END THIRD PARTY */ @@ -209,7 +229,7 @@ const LABEL_PREFIX_SCORE = 1 << 17; const LABEL_CAMELCASE_SCORE = 1 << 16; const LABEL_SCORE_THRESHOLD = 1 << 15; -export function scoreItem(item: T, query: string, accessor: IItemAccessor, cache: ScorerCache): IItemScore { +export function scoreItem(item: T, query: string, fuzzy: boolean, accessor: IItemAccessor, cache: ScorerCache): IItemScore { if (!item || !query) { return NO_ITEM_SCORE; // we need an item and query to score on at least } @@ -221,9 +241,11 @@ export function scoreItem(item: T, query: string, accessor: IItemAccessor, const description = accessor.getItemDescription(item); - let cacheHash = label + query; + let cacheHash: string; if (description) { - cacheHash += description; + cacheHash = `${label}${description}${query}${fuzzy}`; + } else { + cacheHash = `${label}${query}${fuzzy}`; } const cached = cache[cacheHash]; @@ -231,13 +253,13 @@ export function scoreItem(item: T, query: string, accessor: IItemAccessor, return cached; } - const itemScore = doScoreItem(label, description, accessor.getItemPath(item), query, accessor); + const itemScore = doScoreItem(label, description, accessor.getItemPath(item), query, fuzzy, accessor); cache[cacheHash] = itemScore; return itemScore; } -function doScoreItem(label: string, description: string, path: string, query: string, accessor: IItemAccessor): IItemScore { +function doScoreItem(label: string, description: string, path: string, query: string, fuzzy: boolean, accessor: IItemAccessor): IItemScore { // 1.) treat identity matches on full path highest if (path && isEqual(query, path, true)) { @@ -257,7 +279,7 @@ function doScoreItem(label: string, description: string, path: string, query: } // 4.) prefer scores on the label if any - const [labelScore, labelPositions] = _doScore(label, query); + const [labelScore, labelPositions] = _doScore(label, query, fuzzy); if (labelScore) { return { score: labelScore + LABEL_SCORE_THRESHOLD, labelMatch: createMatches(labelPositions) }; } @@ -272,12 +294,12 @@ function doScoreItem(label: string, description: string, path: string, query: const descriptionPrefixLength = descriptionPrefix.length; const descriptionAndLabel = `${descriptionPrefix}${label}`; - let [labelDescriptionScore, labelDescriptionPositions] = _doScore(descriptionAndLabel, query); + let [labelDescriptionScore, labelDescriptionPositions] = _doScore(descriptionAndLabel, query, fuzzy); // 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 */); + const [labelDescriptionScoreInverse, labelDescriptionPositionsInverse] = _doScore(descriptionAndLabel, query, fuzzy, true /* inverse */); if (labelDescriptionScoreInverse && labelDescriptionScoreInverse > labelDescriptionScore) { labelDescriptionScore = labelDescriptionScoreInverse; labelDescriptionPositions = labelDescriptionPositionsInverse; @@ -316,9 +338,9 @@ function doScoreItem(label: string, description: string, path: string, query: return NO_ITEM_SCORE; } -export function compareItemsByScore(itemA: T, itemB: T, query: string, accessor: IItemAccessor, cache: ScorerCache): number { - const scoreA = scoreItem(itemA, query, accessor, cache).score; - const scoreB = scoreItem(itemB, query, accessor, cache).score; +export function compareItemsByScore(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: IItemAccessor, cache: ScorerCache): number { + const scoreA = scoreItem(itemA, query, fuzzy, accessor, cache).score; + const scoreB = scoreItem(itemB, query, fuzzy, accessor, cache).score; // 1.) check for identity matches if (scoreA === PATH_IDENTITY_SCORE || scoreB === PATH_IDENTITY_SCORE) { diff --git a/src/vs/base/test/common/scorer.test.ts b/src/vs/base/parts/quickopen/test/common/quickOpenScorer.test.ts similarity index 71% rename from src/vs/base/test/common/scorer.test.ts rename to src/vs/base/parts/quickopen/test/common/quickOpenScorer.test.ts index 650e531acac..111e9515a6a 100644 --- a/src/vs/base/test/common/scorer.test.ts +++ b/src/vs/base/parts/quickopen/test/common/quickOpenScorer.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import * as scorer from 'vs/base/common/scorer'; +import * as scorer from 'vs/base/parts/quickopen/common/quickOpenScorer'; import URI from 'vs/base/common/uri'; import { basename, dirname } from 'vs/base/common/paths'; @@ -45,25 +45,25 @@ class NullAccessorClass implements scorer.IItemAccessor { const NullAccessor = new NullAccessorClass(); const cache: scorer.ScorerCache = Object.create(null); -suite('Scorer', () => { +suite('Quick Open Scorer', () => { - test('score', function () { + test('score (fuzzy)', function () { const target = 'HeLlo-World'; const scores: scorer.Score[] = []; - scores.push(scorer._doScore(target, 'HelLo-World')); // direct case match - scores.push(scorer._doScore(target, 'hello-world')); // direct mix-case match - scores.push(scorer._doScore(target, 'HW')); // direct case prefix (multiple) - scores.push(scorer._doScore(target, 'hw')); // direct mix-case prefix (multiple) - scores.push(scorer._doScore(target, 'H')); // direct case prefix - scores.push(scorer._doScore(target, 'h')); // direct mix-case prefix - scores.push(scorer._doScore(target, 'W')); // direct case word prefix - scores.push(scorer._doScore(target, 'w')); // direct mix-case word prefix - scores.push(scorer._doScore(target, 'Ld')); // in-string case match (multiple) - scores.push(scorer._doScore(target, 'ld')); // in-string mix-case match - scores.push(scorer._doScore(target, 'L')); // in-string case match - scores.push(scorer._doScore(target, 'l')); // in-string mix-case match - scores.push(scorer._doScore(target, '4')); // no match + scores.push(scorer._doScore(target, 'HelLo-World', true)); // direct case match + scores.push(scorer._doScore(target, 'hello-world', true)); // direct mix-case match + scores.push(scorer._doScore(target, 'HW', true)); // direct case prefix (multiple) + scores.push(scorer._doScore(target, 'hw', true)); // direct mix-case prefix (multiple) + scores.push(scorer._doScore(target, 'H', true)); // direct case prefix + scores.push(scorer._doScore(target, 'h', true)); // direct mix-case prefix + scores.push(scorer._doScore(target, 'W', true)); // direct case word prefix + scores.push(scorer._doScore(target, 'w', true)); // direct mix-case word prefix + scores.push(scorer._doScore(target, 'Ld', true)); // in-string case match (multiple) + scores.push(scorer._doScore(target, 'ld', true)); // in-string mix-case match + scores.push(scorer._doScore(target, 'L', true)); // in-string case match + scores.push(scorer._doScore(target, 'l', true)); // in-string mix-case match + scores.push(scorer._doScore(target, '4', true)); // no match // Assert scoring order let sortedScores = scores.concat().sort((a, b) => b[0] - a[0]); @@ -79,17 +79,45 @@ suite('Scorer', () => { assert.equal(positions[1], 6); }); + test('score (non fuzzy)', function () { + const target = 'HeLlo-World'; + + assert.ok(scorer._doScore(target, 'HelLo-World', false)[0] > 0); + assert.equal(scorer._doScore(target, 'HelLo-World', false)[1].length, 'HelLo-World'.length); + + assert.ok(scorer._doScore(target, 'hello-world', false)[0] > 0); + assert.equal(scorer._doScore(target, 'HW', false)[0], 0); + assert.ok(scorer._doScore(target, 'h', false)[0] > 0); + assert.ok(scorer._doScore(target, 'ello', false)[0] > 0); + assert.ok(scorer._doScore(target, 'ld', false)[0] > 0); + assert.equal(scorer._doScore(target, 'eo', false)[0], 0); + }); + + test('score (non fuzzy, inverse)', function () { + const target = 'HeLlo-World'; + + assert.ok(scorer._doScore(target, 'HelLo-World', false, true)[0] > 0); + assert.equal(scorer._doScore(target, 'HelLo-World', false, true)[1].length, 'HelLo-World'.length); + + assert.ok(scorer._doScore(target, 'hello-world', false, true)[0] > 0); + assert.equal(scorer._doScore(target, 'HW', false, true)[0], 0); + assert.ok(scorer._doScore(target, 'h', false, true)[0] > 0); + assert.ok(scorer._doScore(target, 'ello', false, true)[0] > 0); + assert.ok(scorer._doScore(target, 'ld', false, true)[0] > 0); + assert.equal(scorer._doScore(target, 'eo', false, true)[0], 0); + }); + test('scoreItem - matches are proper', function () { - let res = scorer.scoreItem(null, 'something', ResourceAccessor, cache); + let res = scorer.scoreItem(null, 'something', true, ResourceAccessor, cache); assert.ok(!res.score); const resource = URI.file('/xyz/some/path/someFile123.txt'); - res = scorer.scoreItem(resource, 'something', NullAccessor, cache); + res = scorer.scoreItem(resource, 'something', true, NullAccessor, cache); assert.ok(!res.score); // Path Identity - const identityRes = scorer.scoreItem(resource, ResourceAccessor.getItemPath(resource), ResourceAccessor, cache); + const identityRes = scorer.scoreItem(resource, ResourceAccessor.getItemPath(resource), true, ResourceAccessor, cache); assert.ok(identityRes.score); assert.equal(identityRes.descriptionMatch.length, 1); assert.equal(identityRes.labelMatch.length, 1); @@ -99,7 +127,7 @@ suite('Scorer', () => { assert.equal(identityRes.labelMatch[0].end, ResourceAccessor.getItemLabel(resource).length); // Basename Prefix - const basenamePrefixRes = scorer.scoreItem(resource, 'som', ResourceAccessor, cache); + const basenamePrefixRes = scorer.scoreItem(resource, 'som', true, ResourceAccessor, cache); assert.ok(basenamePrefixRes.score); assert.ok(!basenamePrefixRes.descriptionMatch); assert.equal(basenamePrefixRes.labelMatch.length, 1); @@ -107,7 +135,7 @@ suite('Scorer', () => { assert.equal(basenamePrefixRes.labelMatch[0].end, 'som'.length); // Basename Camelcase - const basenameCamelcaseRes = scorer.scoreItem(resource, 'sF', ResourceAccessor, cache); + const basenameCamelcaseRes = scorer.scoreItem(resource, 'sF', true, ResourceAccessor, cache); assert.ok(basenameCamelcaseRes.score); assert.ok(!basenameCamelcaseRes.descriptionMatch); assert.equal(basenameCamelcaseRes.labelMatch.length, 2); @@ -117,7 +145,7 @@ suite('Scorer', () => { assert.equal(basenameCamelcaseRes.labelMatch[1].end, 5); // Basename Match - const basenameRes = scorer.scoreItem(resource, 'of', ResourceAccessor, cache); + const basenameRes = scorer.scoreItem(resource, 'of', true, ResourceAccessor, cache); assert.ok(basenameRes.score); assert.ok(!basenameRes.descriptionMatch); assert.equal(basenameRes.labelMatch.length, 2); @@ -127,7 +155,7 @@ suite('Scorer', () => { assert.equal(basenameRes.labelMatch[1].end, 5); // Path Match - const pathRes = scorer.scoreItem(resource, 'xyz123', ResourceAccessor, cache); + const pathRes = scorer.scoreItem(resource, 'xyz123', true, ResourceAccessor, cache); assert.ok(pathRes.score); assert.ok(pathRes.descriptionMatch); assert.ok(pathRes.labelMatch); @@ -139,7 +167,7 @@ suite('Scorer', () => { assert.equal(pathRes.descriptionMatch[0].end, 4); // No Match - const noRes = scorer.scoreItem(resource, '987', ResourceAccessor, cache); + const noRes = scorer.scoreItem(resource, '987', true, ResourceAccessor, cache); assert.ok(!noRes.score); assert.ok(!noRes.labelMatch); assert.ok(!noRes.descriptionMatch); @@ -157,7 +185,7 @@ suite('Scorer', () => { // 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); + const pathRes = scorer.scoreItem(resource, 'xspfile123', true, ResourceAccessor, cache); assert.ok(pathRes.score); assert.ok(pathRes.descriptionMatch); assert.ok(pathRes.labelMatch); @@ -177,12 +205,12 @@ suite('Scorer', () => { // Full resource A path let query = ResourceAccessor.getItemPath(resourceA); - let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); @@ -190,12 +218,12 @@ suite('Scorer', () => { // Full resource B path query = ResourceAccessor.getItemPath(resourceB); - res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); assert.equal(res[2], resourceC); @@ -209,12 +237,12 @@ suite('Scorer', () => { // Full resource A basename let query = ResourceAccessor.getItemLabel(resourceA); - let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); @@ -222,12 +250,12 @@ suite('Scorer', () => { // Full resource B basename query = ResourceAccessor.getItemLabel(resourceB); - res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); assert.equal(res[2], resourceC); @@ -241,12 +269,12 @@ suite('Scorer', () => { // resource A camelcase let query = 'fA'; - let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); @@ -254,12 +282,12 @@ suite('Scorer', () => { // resource B camelcase query = 'fB'; - res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); assert.equal(res[2], resourceC); @@ -273,12 +301,12 @@ suite('Scorer', () => { // Resource A part of basename let query = 'fileA'; - let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); @@ -286,12 +314,12 @@ suite('Scorer', () => { // Resource B part of basename query = 'fileB'; - res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); assert.equal(res[2], resourceC); @@ -305,12 +333,12 @@ suite('Scorer', () => { // Resource A part of path let query = 'pathfileA'; - let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); @@ -318,12 +346,12 @@ suite('Scorer', () => { // Resource B part of path query = 'pathfileB'; - res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); assert.equal(res[2], resourceC); @@ -337,12 +365,12 @@ suite('Scorer', () => { // Resource A part of path let query = 'somepath'; - let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); @@ -356,12 +384,12 @@ suite('Scorer', () => { // Resource A part of path let query = 'somepath'; - let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); - res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + res = [resourceC, resourceB, resourceA].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceA); assert.equal(res[1], resourceB); assert.equal(res[2], resourceC); @@ -374,7 +402,7 @@ suite('Scorer', () => { let query = 'co/te'; - let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, ResourceAccessor, cache)); + let res = [resourceA, resourceB, resourceC].sort((r1, r2) => scorer.compareItemsByScore(r1, r2, query, true, ResourceAccessor, cache)); assert.equal(res[0], resourceB); assert.equal(res[1], resourceA); assert.equal(res[2], resourceC); diff --git a/src/vs/workbench/browser/parts/editor/editorPicker.ts b/src/vs/workbench/browser/parts/editor/editorPicker.ts index a3711f77b82..f02a2e68452 100644 --- a/src/vs/workbench/browser/parts/editor/editorPicker.ts +++ b/src/vs/workbench/browser/parts/editor/editorPicker.ts @@ -23,7 +23,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { EditorInput, toResource, IEditorGroup, IEditorStacksModel } from 'vs/workbench/common/editor'; import { stripWildcards } from 'vs/base/common/strings'; -import { compareItemsByScore, scoreItem, ScorerCache } from 'vs/base/common/scorer'; +import { compareItemsByScore, scoreItem, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer'; export class EditorPickerEntry extends QuickOpenEntryGroup { private stacks: IEditorStacksModel; @@ -116,7 +116,7 @@ export abstract class BaseEditorPicker extends QuickOpenHandler { return true; } - const itemScore = scoreItem(e, searchValue, QuickOpenItemAccessor, this.scorerCache); + const itemScore = scoreItem(e, searchValue, true, QuickOpenItemAccessor, this.scorerCache); if (!itemScore.score) { return false; } @@ -133,7 +133,7 @@ export abstract class BaseEditorPicker extends QuickOpenHandler { return stacks.positionOfGroup(e1.group) - stacks.positionOfGroup(e2.group); } - return compareItemsByScore(e1, e2, searchValue, QuickOpenItemAccessor, this.scorerCache); + return compareItemsByScore(e1, e2, searchValue, true, QuickOpenItemAccessor, this.scorerCache); }); } diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index 8bd94aa7944..38d4e6d010d 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -20,7 +20,7 @@ import { Action, IAction } from 'vs/base/common/actions'; import { IIconLabelOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Mode, IEntryRunContext, IAutoFocus, IQuickNavigateConfiguration, IModel } from 'vs/base/parts/quickopen/common/quickOpen'; -import { QuickOpenEntry, QuickOpenModel, QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel'; +import { QuickOpenEntry, QuickOpenModel, QuickOpenEntryGroup, compareEntries, QuickOpenItemAccessorClass } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { QuickOpenWidget, HideReason } from 'vs/base/parts/quickopen/browser/quickOpenWidget'; import { ContributableActionProvider } from 'vs/workbench/browser/actions'; import labels = require('vs/base/common/labels'); @@ -55,6 +55,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree'; import { BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { FileKind, IFileService } from 'vs/platform/files/common/files'; +import { scoreItem, ScorerCache, compareItemsByScore } from 'vs/base/parts/quickopen/common/quickOpenScorer'; const HELP_PREFIX = '?'; @@ -458,7 +459,7 @@ export class QuickOpenController extends Component implements IQuickOpenService return pickA.index - pickB.index; // restore natural order } - return QuickOpenEntry.compare(pickA, pickB, normalizedSearchValue); + return compareEntries(pickA, pickB, normalizedSearchValue); }); this.pickOpenWidget.refresh(model, value ? { autoFocusFirstEntry: true } : autoFocus); @@ -1164,6 +1165,7 @@ class PickOpenActionProvider implements IActionProvider { } class EditorHistoryHandler { + private scorerCache: ScorerCache; constructor( @IHistoryService private historyService: IHistoryService, @@ -1171,11 +1173,12 @@ class EditorHistoryHandler { @IWorkspaceContextService private contextService: IWorkspaceContextService, @IFileService private fileService: IFileService ) { + this.scorerCache = Object.create(null); } public getResults(searchValue?: string): QuickOpenEntry[] { if (searchValue) { - searchValue = searchValue.replace(/ /g, ''); // get rid of all whitespace + searchValue = strings.stripWildcards(searchValue.replace(/ /g, '')); // get rid of all whitespace and wildcards } // Just return all if we are not searching @@ -1184,49 +1187,57 @@ class EditorHistoryHandler { return history.map(input => this.instantiationService.createInstance(EditorHistoryEntry, input)); } - const searchInPath = searchValue.indexOf(paths.nativeSep) >= 0; + // Otherwise filter by search value and sort by score. Include matches on description + // in case the user is explicitly including path separators. + const accessor = searchValue.indexOf(paths.nativeSep) >= 0 ? MatchOnDescription : DoNotMatchOnDescription; + return history - const results: QuickOpenEntry[] = []; - history.forEach(input => { - let resource: URI; - if (input instanceof EditorInput) { - resource = resourceForEditorHistory(input, this.fileService); - } else { - resource = (input as IResourceInput).resource; - } + // For now, only support to match on inputs that provide resource information + .filter(input => { + let resource: URI; + if (input instanceof EditorInput) { + resource = resourceForEditorHistory(input, this.fileService); + } else { + resource = (input as IResourceInput).resource; + } - if (!resource) { - return; //For now, only support to match on inputs that provide resource information - } + return !!resource; + }) - let searchTargetToMatch: string; - if (searchInPath) { - searchTargetToMatch = labels.getPathLabel(resource, this.contextService); - } else if (input instanceof EditorInput) { - searchTargetToMatch = input.getName(); - } else { - searchTargetToMatch = paths.basename((input as IResourceInput).resource.fsPath); - } + // Conver to quick open entries + .map(input => this.instantiationService.createInstance(EditorHistoryEntry, input)) - // Check if this entry is a match for the search value - if (!filters.matchesFuzzy(searchValue, searchTargetToMatch)) { - return; - } + // Make sure the search value is matching + .filter(e => { + const itemScore = scoreItem(e, searchValue, false, accessor, this.scorerCache); + if (!itemScore.score) { + return false; + } - const entry = this.instantiationService.createInstance(EditorHistoryEntry, input); + e.setHighlights(itemScore.labelMatch, itemScore.descriptionMatch); - const { labelHighlights, descriptionHighlights } = QuickOpenEntry.highlight(entry, searchValue); - entry.setHighlights(labelHighlights, descriptionHighlights); + return true; + }) - results.push(entry); - }); - - // Sort - const normalizedSearchValue = strings.stripWildcards(searchValue.toLowerCase()); - return results.sort((elementA: EditorHistoryEntry, elementB: EditorHistoryEntry) => QuickOpenEntry.compare(elementA, elementB, normalizedSearchValue)); + // Sort by score + .sort((e1, e2) => compareItemsByScore(e1, e2, searchValue, false, accessor, this.scorerCache)); } } +class EditorHistoryItemAccessorClass extends QuickOpenItemAccessorClass { + + constructor(private allowMatchOnDescription: boolean) { + super(); + } + + public getItemDescription(entry: QuickOpenEntry): string { + return this.allowMatchOnDescription ? entry.getDescription() : void 0; + } +} + +const MatchOnDescription = new EditorHistoryItemAccessorClass(true); +const DoNotMatchOnDescription = new EditorHistoryItemAccessorClass(false); + export class EditorHistoryEntryGroup extends QuickOpenEntryGroup { // Marker class } diff --git a/src/vs/workbench/parts/search/browser/openAnythingHandler.ts b/src/vs/workbench/parts/search/browser/openAnythingHandler.ts index 6894223af76..1814995bbef 100644 --- a/src/vs/workbench/parts/search/browser/openAnythingHandler.ts +++ b/src/vs/workbench/parts/search/browser/openAnythingHandler.ts @@ -25,7 +25,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchSearchConfiguration } from 'vs/workbench/parts/search/common/search'; import { IRange } from 'vs/editor/common/core/range'; -import { compareItemsByScore, scoreItem, ScorerCache } from 'vs/base/common/scorer'; +import { compareItemsByScore, scoreItem, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer'; export import OpenSymbolHandler = openSymbolHandler.OpenSymbolHandler; // OpenSymbolHandler is used from an extension and must be in the main bundle file so it can load @@ -218,7 +218,7 @@ export class OpenAnythingHandler extends QuickOpenHandler { // Sort const unsortedResultTime = Date.now(); const normalizedSearchValue = strings.stripWildcards(searchValue); - const compare = (elementA: QuickOpenEntry, elementB: QuickOpenEntry) => compareItemsByScore(elementA, elementB, normalizedSearchValue, QuickOpenItemAccessor, this.scorerCache); + const compare = (elementA: QuickOpenEntry, elementB: QuickOpenEntry) => compareItemsByScore(elementA, elementB, normalizedSearchValue, true, QuickOpenItemAccessor, this.scorerCache); const viewResults = arrays.top(mergedResults, compare, OpenAnythingHandler.MAX_DISPLAYED_RESULTS); const sortedResultTime = Date.now(); @@ -227,7 +227,7 @@ export class OpenAnythingHandler extends QuickOpenHandler { if (entry instanceof FileEntry) { entry.setRange(searchWithRange ? searchWithRange.range : null); - const itemScore = scoreItem(entry, normalizedSearchValue, QuickOpenItemAccessor, this.scorerCache); + const itemScore = scoreItem(entry, normalizedSearchValue, true, QuickOpenItemAccessor, this.scorerCache); entry.setHighlights(itemScore.labelMatch, itemScore.descriptionMatch); } }); diff --git a/src/vs/workbench/parts/search/browser/openSymbolHandler.ts b/src/vs/workbench/parts/search/browser/openSymbolHandler.ts index f2afa0dbfae..4e552a9ca1a 100644 --- a/src/vs/workbench/parts/search/browser/openSymbolHandler.ts +++ b/src/vs/workbench/parts/search/browser/openSymbolHandler.ts @@ -10,7 +10,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ThrottledDelayer } from 'vs/base/common/async'; import { QuickOpenHandler, EditorQuickOpenEntry } from 'vs/workbench/browser/quickopen'; -import { QuickOpenModel, QuickOpenEntry } from 'vs/base/parts/quickopen/browser/quickOpenModel'; +import { QuickOpenModel, QuickOpenEntry, compareEntries } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { IAutoFocus, Mode, IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen'; import filters = require('vs/base/common/filters'); import strings = require('vs/base/common/strings'); @@ -118,7 +118,7 @@ class SymbolEntry extends EditorQuickOpenEntry { return elementAType.localeCompare(elementBType); } - return QuickOpenEntry.compare(elementA, elementB, searchValue); + return compareEntries(elementA, elementB, searchValue); } } diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index f1d235d42bb..0f100b63bc7 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -23,7 +23,7 @@ import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/text import { IRawSearchService, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchProgressItem, ISerializedSearchComplete, ISearchEngine, IFileSearchProgressItem, ITelemetryEvent } from './search'; import { ICachedSearchStats, IProgress } from 'vs/platform/search/common/search'; import { fuzzyContains } from 'vs/base/common/strings'; -import { compareItemsByScore, IItemAccessor, ScorerCache } from 'vs/base/common/scorer'; +import { compareItemsByScore, IItemAccessor, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer'; export class SearchService implements IRawSearchService { @@ -254,7 +254,7 @@ export class SearchService implements IRawSearchService { // this is very important because we are also limiting the number of results by config.maxResults // and as such we want the top items to be included in this result set if the number of items // exceeds config.maxResults. - const compare = (matchA: IRawFileMatch, matchB: IRawFileMatch) => compareItemsByScore(matchA, matchB, strings.stripWildcards(config.filePattern), FileMatchItemAccessor, scorerCache); + const compare = (matchA: IRawFileMatch, matchB: IRawFileMatch) => compareItemsByScore(matchA, matchB, strings.stripWildcards(config.filePattern), true, FileMatchItemAccessor, scorerCache); return arrays.topAsync(results, compare, config.maxResults, 10000); }