提交 a19857db 编写于 作者: B Benjamin Pasero

scorer - introduce normalized path

上级 d7d1147d
......@@ -25,15 +25,15 @@ export function score(target: string, query: IPreparedQuery, fuzzy: boolean): Sc
return scoreMultiple(target, query.values, fuzzy);
}
return scoreSingle(target, query.value, query.valueLowercase, 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 { value, valueLowercase } of query) {
const [scoreValue, positions] = scoreSingle(target, value, valueLowercase, fuzzy);
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
......@@ -338,11 +338,26 @@ const LABEL_CAMELCASE_SCORE = 1 << 16;
const LABEL_SCORE_THRESHOLD = 1 << 15;
export interface IPreparedQueryPiece {
/**
* The original query as provided as input.
*/
original: string;
originalLowercase: string;
value: string;
valueLowercase: string;
/**
* Original normalized to platform separators:
* - Windows: \
* - Posix: /
*/
pathNormalized: string;
/**
* In addition to the normalized path, will have
* whitespace and wildcards removed.
*/
normalized: string;
normalizedLowercase: string;
}
export interface IPreparedQuery extends IPreparedQueryPiece {
......@@ -364,17 +379,21 @@ export function prepareQuery(original: string): IPreparedQuery {
}
const originalLowercase = original.toLowerCase();
const value = prepareQueryValue(original);
const valueLowercase = value.toLowerCase();
const containsPathSeparator = value.indexOf(sep) >= 0;
const { pathNormalized, normalized, normalizedLowercase } = normalizeQuery(original);
const containsPathSeparator = pathNormalized.indexOf(sep) >= 0;
let values: IPreparedQueryPiece[] | undefined = undefined;
const originalSplit = original.split(MULTIPL_QUERY_VALUES_SEPARATOR);
if (originalSplit.length > 1) {
for (const originalPiece of originalSplit) {
const valuePiece = prepareQueryValue(originalPiece);
if (valuePiece) {
const {
pathNormalized: pathNormalizedPiece,
normalized: normalizedPiece,
normalizedLowercase: normalizedLowercasePiece
} = normalizeQuery(originalPiece);
if (normalizedPiece) {
if (!values) {
values = [];
}
......@@ -382,29 +401,36 @@ export function prepareQuery(original: string): IPreparedQuery {
values.push({
original: originalPiece,
originalLowercase: originalPiece.toLowerCase(),
value: valuePiece,
valueLowercase: valuePiece.toLowerCase()
pathNormalized: pathNormalizedPiece,
normalized: normalizedPiece,
normalizedLowercase: normalizedLowercasePiece
});
}
}
}
return { original, originalLowercase, value, valueLowercase, values, containsPathSeparator };
return { original, originalLowercase, pathNormalized, normalized, normalizedLowercase, values, containsPathSeparator };
}
function prepareQueryValue(original: string): string {
let value = stripWildcards(original).replace(/\s/g, ''); // get rid of all wildcards and whitespace
function normalizeQuery(original: string): { pathNormalized: string, normalized: string, normalizedLowercase: string } {
let pathNormalized: string;
if (isWindows) {
value = value.replace(/\//g, sep); // Help Windows users to search for paths when using slash
pathNormalized = original.replace(/\//g, sep); // Help Windows users to search for paths when using slash
} else {
value = value.replace(/\\/g, sep); // Help macOS/Linux users to search for paths when using backslash
pathNormalized = original.replace(/\\/g, sep); // Help macOS/Linux users to search for paths when using backslash
}
return value;
const normalized = stripWildcards(pathNormalized).replace(/\s/g, '');
return {
pathNormalized,
normalized,
normalizedLowercase: normalized.toLowerCase()
};
}
export function scoreItem<T>(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor<T>, cache: ScorerCache): IItemScore {
if (!item || !query.value) {
if (!item || !query.normalized) {
return NO_ITEM_SCORE; // we need an item and query to score on at least
}
......@@ -417,9 +443,9 @@ export function scoreItem<T>(item: T, query: IPreparedQuery, fuzzy: boolean, acc
let cacheHash: string;
if (description) {
cacheHash = `${label}${description}${query.value}${fuzzy}`;
cacheHash = `${label}${description}${query.normalized}${fuzzy}`;
} else {
cacheHash = `${label}${query.value}${fuzzy}`;
cacheHash = `${label}${query.normalized}${fuzzy}`;
}
const cached = cache[cacheHash];
......@@ -455,7 +481,7 @@ function createMatches(offsets: undefined | number[]): IMatch[] {
function doScoreItem(label: string, description: string | undefined, path: string | undefined, query: IPreparedQuery, fuzzy: boolean): IItemScore {
// 1.) treat identity matches on full path highest
if (path && (isLinux ? query.original === path : equalsIgnoreCase(query.original, path))) {
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 };
}
......@@ -464,13 +490,13 @@ function doScoreItem(label: string, description: string | undefined, path: strin
if (preferLabelMatches) {
// 2.) treat prefix matches on the label second highest
const prefixLabelMatch = matchesPrefix(query.value, label);
const prefixLabelMatch = matchesPrefix(query.normalized, label);
if (prefixLabelMatch) {
return { score: LABEL_PREFIX_SCORE, labelMatch: prefixLabelMatch };
}
// 3.) treat camelcase matches on the label third highest
const camelcaseLabelMatch = matchesCamelCase(query.value, label);
const camelcaseLabelMatch = matchesCamelCase(query.normalized, label);
if (camelcaseLabelMatch) {
return { score: LABEL_CAMELCASE_SCORE, labelMatch: camelcaseLabelMatch };
}
......@@ -702,17 +728,17 @@ function fallbackCompare<T>(itemA: T, itemB: T, query: IPreparedQuery, accessor:
// compare by label
if (labelA !== labelB) {
return compareAnything(labelA, labelB, query.value);
return compareAnything(labelA, labelB, query.normalized);
}
// compare by description
if (descriptionA && descriptionB && descriptionA !== descriptionB) {
return compareAnything(descriptionA, descriptionB, query.value);
return compareAnything(descriptionA, descriptionB, query.normalized);
}
// compare by path
if (pathA && pathB && pathA !== pathB) {
return compareAnything(pathA, pathB, query.value);
return compareAnything(pathA, pathB, query.normalized);
}
// equal
......
......@@ -857,41 +857,58 @@ suite('Fuzzy Scorer', () => {
});
test('prepareQuery', () => {
assert.equal(scorer.prepareQuery(' f*a ').value, 'fa');
assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa');
assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts');
assert.equal(scorer.prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase());
assert.equal(scorer.prepareQuery('model Tester.ts').value, 'modelTester.ts');
assert.equal(scorer.prepareQuery('Model Tester.ts').valueLowercase, 'modeltester.ts');
assert.equal(scorer.prepareQuery('model Tester.ts').normalized, 'modelTester.ts');
assert.equal(scorer.prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts');
assert.equal(scorer.prepareQuery('ModelTester.ts').containsPathSeparator, false);
assert.equal(scorer.prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true);
// with spaces
let query = scorer.prepareQuery('He*llo World');
assert.equal(query.original, 'He*llo World');
assert.equal(query.value, 'HelloWorld');
assert.equal(query.valueLowercase, 'HelloWorld'.toLowerCase());
assert.equal(query.normalized, 'HelloWorld');
assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase());
assert.equal(query.values?.length, 2);
assert.equal(query.values?.[0].original, 'He*llo');
assert.equal(query.values?.[0].value, 'Hello');
assert.equal(query.values?.[0].valueLowercase, 'Hello'.toLowerCase());
assert.equal(query.values?.[0].normalized, 'Hello');
assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase());
assert.equal(query.values?.[1].original, 'World');
assert.equal(query.values?.[1].value, 'World');
assert.equal(query.values?.[1].valueLowercase, 'World'.toLowerCase());
assert.equal(query.values?.[1].normalized, 'World');
assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase());
// with spaces that are empty
query = scorer.prepareQuery(' Hello World ');
assert.equal(query.original, ' Hello World ');
assert.equal(query.originalLowercase, ' Hello World '.toLowerCase());
assert.equal(query.value, 'HelloWorld');
assert.equal(query.valueLowercase, 'HelloWorld'.toLowerCase());
assert.equal(query.normalized, 'HelloWorld');
assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase());
assert.equal(query.values?.length, 2);
assert.equal(query.values?.[0].original, 'Hello');
assert.equal(query.values?.[0].originalLowercase, 'Hello'.toLowerCase());
assert.equal(query.values?.[0].value, 'Hello');
assert.equal(query.values?.[0].valueLowercase, 'Hello'.toLowerCase());
assert.equal(query.values?.[0].normalized, 'Hello');
assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase());
assert.equal(query.values?.[1].original, 'World');
assert.equal(query.values?.[1].originalLowercase, 'World'.toLowerCase());
assert.equal(query.values?.[1].value, 'World');
assert.equal(query.values?.[1].valueLowercase, 'World'.toLowerCase());
assert.equal(query.values?.[1].normalized, 'World');
assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase());
// Path related
if (isWindows) {
assert.equal(scorer.prepareQuery('C:\\some\\path').pathNormalized, 'C:\\some\\path');
assert.equal(scorer.prepareQuery('C:\\some\\path').normalized, 'C:\\some\\path');
assert.equal(scorer.prepareQuery('C:\\some\\path').containsPathSeparator, true);
assert.equal(scorer.prepareQuery('C:/some/path').pathNormalized, 'C:\\some\\path');
assert.equal(scorer.prepareQuery('C:/some/path').normalized, 'C:\\some\\path');
assert.equal(scorer.prepareQuery('C:/some/path').containsPathSeparator, true);
} else {
assert.equal(scorer.prepareQuery('/some/path').pathNormalized, '/some/path');
assert.equal(scorer.prepareQuery('/some/path').normalized, '/some/path');
assert.equal(scorer.prepareQuery('/some/path').containsPathSeparator, true);
assert.equal(scorer.prepareQuery('\\some\\path').pathNormalized, '/some/path');
assert.equal(scorer.prepareQuery('\\some\\path').normalized, '/some/path');
assert.equal(scorer.prepareQuery('\\some\\path').containsPathSeparator, true);
}
});
});
......@@ -62,7 +62,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro
// Filtering
const filteredEditorEntries = this.doGetEditorPickItems().filter(entry => {
if (!query.value) {
if (!query.normalized) {
return true;
}
......@@ -79,7 +79,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro
});
// Sorting
if (query.value) {
if (query.normalized) {
const groups = this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).map(group => group.id);
filteredEditorEntries.sort((entryA, entryB) => {
if (entryA.groupId !== entryB.groupId) {
......
......@@ -402,7 +402,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
const configuration = this.configuration;
// Just return all history entries if not searching
if (!query.value) {
if (!query.normalized) {
return this.historyService.getHistory().map(editor => this.createAnythingPick(editor, configuration));
}
......@@ -448,9 +448,9 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
//#region File Search
private fileQueryDelayer = this._register(new ThrottledDelayer<URI[]>(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY));
private readonly fileQueryDelayer = this._register(new ThrottledDelayer<URI[]>(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY));
private fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder);
private readonly fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder);
private createFileQueryCache(): FileQueryCacheState {
return new FileQueryCacheState(
......@@ -462,7 +462,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
}
private async getFilePicks(query: IPreparedQuery, excludes: ResourceMap<boolean>, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
if (!query.value) {
if (!query.normalized) {
return [];
}
......@@ -692,9 +692,9 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
private async getWorkspaceSymbolPicks(query: IPreparedQuery, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
const configuration = this.configuration;
if (
!query.value || // we need a value for search for
!configuration.includeSymbols || // we need to enable symbols in search
this.pickState.lastRange // a range is an indicator for just searching for files
!query.normalized || // we need a value for search for
!configuration.includeSymbols || // we need to enable symbols in search
this.pickState.lastRange // a range is an indicator for just searching for files
) {
return [];
}
......
......@@ -77,7 +77,7 @@ export class FileWalker {
this.errors = [];
if (this.filePattern) {
this.normalizedFilePatternLowercase = prepareQuery(this.filePattern).valueLowercase;
this.normalizedFilePatternLowercase = prepareQuery(this.filePattern).normalizedLowercase;
}
this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
......
......@@ -312,7 +312,7 @@ export class SearchService implements IRawSearchService {
// Pattern match on results
const results: IRawFileMatch[] = [];
const normalizedSearchValueLowercase = prepareQuery(searchValue).valueLowercase;
const normalizedSearchValueLowercase = prepareQuery(searchValue).normalizedLowercase;
for (const entry of cachedEntries) {
// Check if this entry is a match for the search value
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册