提交 7a2b6fd7 编写于 作者: B Benjamin Pasero

more predictable recent files sorting (#10690, #20546)

上级 d869040c
......@@ -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:",
"",
......
......@@ -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;
}
......
......@@ -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
......@@ -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<T>(item: T, query: string, accessor: IItemAccessor<T>, cache: ScorerCache): IItemScore {
export function scoreItem<T>(item: T, query: string, fuzzy: boolean, accessor: IItemAccessor<T>, 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<T>(item: T, query: string, accessor: IItemAccessor<T>,
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<T>(item: T, query: string, accessor: IItemAccessor<T>,
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<T>(label: string, description: string, path: string, query: string, accessor: IItemAccessor<T>): IItemScore {
function doScoreItem<T>(label: string, description: string, path: string, query: string, fuzzy: boolean, accessor: IItemAccessor<T>): IItemScore {
// 1.) treat identity matches on full path highest
if (path && isEqual(query, path, true)) {
......@@ -257,7 +279,7 @@ function doScoreItem<T>(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<T>(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<T>(label: string, description: string, path: string, query:
return NO_ITEM_SCORE;
}
export function compareItemsByScore<T>(itemA: T, itemB: T, query: string, accessor: IItemAccessor<T>, cache: ScorerCache): number {
const scoreA = scoreItem(itemA, query, accessor, cache).score;
const scoreB = scoreItem(itemB, query, accessor, cache).score;
export function compareItemsByScore<T>(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: IItemAccessor<T>, 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) {
......
......@@ -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<URI> {
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);
......
......@@ -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);
});
}
......
......@@ -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 by score
.sort((e1, e2) => compareItemsByScore(e1, e2, searchValue, false, accessor, this.scorerCache));
}
}
class EditorHistoryItemAccessorClass extends QuickOpenItemAccessorClass {
// Sort
const normalizedSearchValue = strings.stripWildcards(searchValue.toLowerCase());
return results.sort((elementA: EditorHistoryEntry, elementB: EditorHistoryEntry) => QuickOpenEntry.compare(elementA, elementB, normalizedSearchValue));
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
}
......
......@@ -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);
}
});
......
......@@ -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);
}
}
......
......@@ -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);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册