提交 34a17728 编写于 作者: B Benjamin Pasero

fix #94046

上级 e1e47d3b
......@@ -9,7 +9,6 @@ import { sep } from 'vs/base/common/path';
import { isWindows, isLinux } from 'vs/base/common/platform';
import { stripWildcards, equalsIgnoreCase } from 'vs/base/common/strings';
import { CharCode } from 'vs/base/common/charCode';
import { distinctES6 } from 'vs/base/common/arrays';
export type Score = [number /* score */, number[] /* match positions */];
export type ScorerCache = { [key: string]: IItemScore };
......@@ -20,40 +19,7 @@ const NO_SCORE: Score = [NO_MATCH, []];
// const DEBUG = false;
// const DEBUG_MATRIX = false;
export function score(target: string, query: IPreparedQuery, fuzzy: boolean): Score {
if (query.values && query.values.length > 1) {
return scoreMultiple(target, query.values, fuzzy);
}
return scoreSingle(target, query.normalized, query.normalizedLowercase, fuzzy);
}
function scoreMultiple(target: string, query: IPreparedQueryPiece[], fuzzy: boolean): Score {
let totalScore = NO_MATCH;
const totalPositions: number[] = [];
for (const { normalized, normalizedLowercase } of query) {
const [scoreValue, positions] = scoreSingle(target, normalized, normalizedLowercase, fuzzy);
if (scoreValue === NO_MATCH) {
// if a single query value does not match, return with
// no score entirely, we require all queries to match
return NO_SCORE;
}
totalScore += scoreValue;
totalPositions.push(...positions);
}
if (totalScore === NO_MATCH) {
return NO_SCORE;
}
// if we have a score, ensure that the positions are
// sorted in ascending order and distinct
return [totalScore, distinctES6(totalPositions).sort((a, b) => a - b)];
}
function scoreSingle(target: string, query: string, queryLower: string, fuzzy: boolean): Score {
export function score(target: string, query: string, queryLower: string, fuzzy: boolean): Score {
if (!target || !query) {
return NO_SCORE; // return early if target or query are undefined
}
......@@ -459,56 +425,80 @@ export function scoreItem<T>(item: T, query: IPreparedQuery, fuzzy: boolean, acc
return itemScore;
}
function createMatches(offsets: undefined | number[]): IMatch[] {
let ret: IMatch[] = [];
if (!offsets) {
return ret;
function doScoreItem(label: string, description: string | undefined, path: string | undefined, query: IPreparedQuery, fuzzy: boolean): IItemScore {
const preferLabelMatches = !path || !query.containsPathSeparator;
// Treat identity matches on full path highest
if (path && (isLinux ? query.pathNormalized === path : equalsIgnoreCase(query.pathNormalized, path))) {
return { score: PATH_IDENTITY_SCORE, labelMatch: [{ start: 0, end: label.length }], descriptionMatch: description ? [{ start: 0, end: description.length }] : undefined };
}
let last: IMatch | undefined;
for (const pos of offsets) {
if (last && last.end === pos) {
last.end += 1;
} else {
last = { start: pos, end: pos + 1 };
ret.push(last);
}
// Score: multiple inputs
if (query.values && query.values.length > 1) {
return doScoreMultiple(label, description, path, query.values, preferLabelMatches, fuzzy);
}
return ret;
// Score: single input
return doScoreSingle(label, description, path, query, preferLabelMatches, fuzzy);
}
function doScoreItem(label: string, description: string | undefined, path: string | undefined, query: IPreparedQuery, fuzzy: boolean): IItemScore {
function doScoreMultiple(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece[], preferLabelMatches: boolean, fuzzy: boolean): IItemScore {
let totalScore = NO_MATCH;
const totalLabelMatches: IMatch[] = [];
const totalDescriptionMatches: IMatch[] = [];
// 1.) treat identity matches on full path highest
if (path && (isLinux ? query.pathNormalized === path : equalsIgnoreCase(query.pathNormalized, path))) {
return { score: PATH_IDENTITY_SCORE, labelMatch: [{ start: 0, end: label.length }], descriptionMatch: description ? [{ start: 0, end: description.length }] : undefined };
for (const queryPiece of query) {
const { score, labelMatch, descriptionMatch } = doScoreSingle(label, description, path, queryPiece, preferLabelMatches, fuzzy);
if (score === NO_MATCH) {
// if a single query value does not match, return with
// no score entirely, we require all queries to match
return NO_ITEM_SCORE;
}
totalScore += score;
if (labelMatch) {
totalLabelMatches.push(...labelMatch);
}
if (descriptionMatch) {
totalDescriptionMatches.push(...descriptionMatch);
}
}
// We only consider label matches if the query is not including file path separators
const preferLabelMatches = !path || !query.containsPathSeparator;
// if we have a score, ensure that the positions are
// sorted in ascending order and distinct
return {
score: totalScore,
labelMatch: normalizeMatches(totalLabelMatches),
descriptionMatch: normalizeMatches(totalDescriptionMatches)
};
}
function doScoreSingle(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece, preferLabelMatches: boolean, fuzzy: boolean): IItemScore {
// Prefer label matches if told so
if (preferLabelMatches) {
// 2.) treat prefix matches on the label second highest
// Treat prefix matches on the label second highest
const prefixLabelMatch = matchesPrefix(query.normalized, label);
if (prefixLabelMatch) {
return { score: LABEL_PREFIX_SCORE, labelMatch: prefixLabelMatch };
}
// 3.) treat camelcase matches on the label third highest
// Treat camelcase matches on the label third highest
const camelcaseLabelMatch = matchesCamelCase(query.normalized, label);
if (camelcaseLabelMatch) {
return { score: LABEL_CAMELCASE_SCORE, labelMatch: camelcaseLabelMatch };
}
// 4.) prefer scores on the label if any
const [labelScore, labelPositions] = score(label, query, fuzzy);
// Prefer scores on the label if any
const [labelScore, labelPositions] = score(label, query.normalized, query.normalizedLowercase, fuzzy);
if (labelScore) {
return { score: labelScore + LABEL_SCORE_THRESHOLD, labelMatch: createMatches(labelPositions) };
}
}
// 5.) finally compute description + label scores if we have a description
// Finally compute description + label scores if we have a description
if (description) {
let descriptionPrefix = description;
if (!!path) {
......@@ -518,7 +508,7 @@ function doScoreItem(label: string, description: string | undefined, path: strin
const descriptionPrefixLength = descriptionPrefix.length;
const descriptionAndLabel = `${descriptionPrefix}${label}`;
const [labelDescriptionScore, labelDescriptionPositions] = score(descriptionAndLabel, query, fuzzy);
const [labelDescriptionScore, labelDescriptionPositions] = score(descriptionAndLabel, query.normalized, query.normalizedLowercase, fuzzy);
if (labelDescriptionScore) {
const labelDescriptionMatches = createMatches(labelDescriptionPositions);
const labelMatch: IMatch[] = [];
......@@ -551,6 +541,37 @@ function doScoreItem(label: string, description: string | undefined, path: strin
return NO_ITEM_SCORE;
}
function createMatches(offsets: undefined | number[]): IMatch[] {
let ret: IMatch[] = [];
if (!offsets) {
return ret;
}
let last: IMatch | undefined;
for (const pos of offsets) {
if (last && last.end === pos) {
last.end += 1;
} else {
last = { start: pos, end: pos + 1 };
ret.push(last);
}
}
return ret;
}
function normalizeMatches(matches: IMatch[]): IMatch[] {
const positions = new Set<number>();
for (const match of matches) {
for (let i = match.start; i < match.end; i++) {
positions.add(i);
}
}
return createMatches(Array.from(positions.values()).sort((a, b) => a - b));
}
export function compareItemsByScore<T>(itemA: T, itemB: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor<T>, cache: ScorerCache): number {
const itemScoreA = scoreItem(itemA, query, fuzzy, accessor, cache);
const itemScoreB = scoreItem(itemB, query, fuzzy, accessor, cache);
......
......@@ -43,7 +43,9 @@ class NullAccessorClass implements scorer.IItemAccessor<URI> {
}
function _doScore(target: string, query: string, fuzzy: boolean): scorer.Score {
return scorer.score(target, scorer.prepareQuery(query), fuzzy);
const preparedQuery = scorer.prepareQuery(query);
return scorer.score(target, preparedQuery.normalized, preparedQuery.normalizedLowercase, fuzzy);
}
function scoreItem<T>(item: T, query: string, fuzzy: boolean, accessor: scorer.IItemAccessor<T>, cache: scorer.ScorerCache): scorer.IItemScore {
......@@ -109,42 +111,6 @@ suite('Fuzzy Scorer', () => {
assert.equal(_doScore(target, 'eo', false)[0], 0);
});
test('score (fuzzy, multiple)', function () {
const target = 'HeLlo-World';
const [firstSingleScore, firstSinglePositions] = _doScore(target, 'HelLo', true);
const [secondSingleScore, secondSinglePositions] = _doScore(target, 'World', true);
const firstAndSecondSinglePositions = [...firstSinglePositions, ...secondSinglePositions];
let [multiScore, multiPositions] = _doScore(target, 'HelLo World', true);
function assertScore() {
assert.ok(multiScore >= firstSingleScore + secondSingleScore);
for (let i = 0; i < multiPositions.length; i++) {
assert.equal(multiPositions[i], firstAndSecondSinglePositions[i]);
}
}
function assertNoScore() {
assert.equal(multiScore, 0);
assert.equal(multiPositions.length, 0);
}
assertScore();
[multiScore, multiPositions] = _doScore(target, 'World HelLo', true);
assertScore();
[multiScore, multiPositions] = _doScore(target, 'World HelLo World', true);
assertScore();
[multiScore, multiPositions] = _doScore(target, 'World HelLo Nothing', true);
assertNoScore();
[multiScore, multiPositions] = _doScore(target, 'More Nothing', true);
assertNoScore();
});
test('scoreItem - matches are proper', function () {
let res = scoreItem(null, 'something', true, ResourceAccessor, cache);
assert.ok(!res.score);
......@@ -217,6 +183,49 @@ suite('Fuzzy Scorer', () => {
assert.ok(pathRes.score > noRes.score);
});
test('scoreItem - multiple', function () {
const resource = URI.file('/xyz/some/path/someFile123.txt');
let res1 = scoreItem(resource, 'xyz some', true, ResourceAccessor, cache);
assert.ok(res1.score);
assert.equal(res1.labelMatch?.length, 1);
assert.equal(res1.labelMatch![0].start, 0);
assert.equal(res1.labelMatch![0].end, 4);
assert.equal(res1.descriptionMatch?.length, 1);
assert.equal(res1.descriptionMatch![0].start, 1);
assert.equal(res1.descriptionMatch![0].end, 4);
let res2 = scoreItem(resource, 'some xyz', true, ResourceAccessor, cache);
assert.ok(res2.score);
assert.equal(res1.score, res2.score);
assert.equal(res2.labelMatch?.length, 1);
assert.equal(res2.labelMatch![0].start, 0);
assert.equal(res2.labelMatch![0].end, 4);
assert.equal(res2.descriptionMatch?.length, 1);
assert.equal(res2.descriptionMatch![0].start, 1);
assert.equal(res2.descriptionMatch![0].end, 4);
let res3 = scoreItem(resource, 'some xyz file file123', true, ResourceAccessor, cache);
assert.ok(res3.score);
assert.ok(res3.score > res2.score);
assert.equal(res3.labelMatch?.length, 1);
assert.equal(res3.labelMatch![0].start, 0);
assert.equal(res3.labelMatch![0].end, 11);
assert.equal(res3.descriptionMatch?.length, 1);
assert.equal(res3.descriptionMatch![0].start, 1);
assert.equal(res3.descriptionMatch![0].end, 4);
let res4 = scoreItem(resource, 'path z y', true, ResourceAccessor, cache);
assert.ok(res4.score);
assert.ok(res4.score < res2.score);
assert.equal(res4.labelMatch?.length, 0);
assert.equal(res4.descriptionMatch?.length, 2);
assert.equal(res4.descriptionMatch![0].start, 2);
assert.equal(res4.descriptionMatch![0].end, 4);
assert.equal(res4.descriptionMatch![1].start, 10);
assert.equal(res4.descriptionMatch![1].end, 14);
});
test('scoreItem - invalid input', function () {
let res = scoreItem(null, null!, true, ResourceAccessor, cache);
......
......@@ -241,7 +241,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
// Score by container if specified
if (includeSymbol && containerQuery) {
if (containerLabel) {
if (containerLabel && containerQuery.original.length > 0) {
containerScore = fuzzyScore(containerQuery.original, containerQuery.originalLowercase, filterPos, containerLabel, containerLabel.toLowerCase(), 0, true);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册