提交 82a31e8e 编写于 作者: J Johannes Rieken 提交者: GitHub

Merge pull request #22884 from Microsoft/joh/fuzzy

fuzzyScore
......@@ -357,3 +357,110 @@ export function matchesFuzzy(word: string, wordToMatchAgainst: string, enableSep
// Default Filter
return enableSeparateSubstringMatching ? fuzzySeparateFilter(word, wordToMatchAgainst) : fuzzyContiguousFilter(word, wordToMatchAgainst);
}
export function matchesFuzzy2(pattern: string, word: string): number[] {
pattern = pattern.toLowerCase();
word = word.toLowerCase();
let matches: number[] = [];
let patternPos = 0;
let wordPos = 0;
while (patternPos < pattern.length && wordPos < word.length) {
if (pattern[patternPos] === word[wordPos]) {
patternPos += 1;
matches.push(wordPos);
}
wordPos += 1;
}
if (patternPos !== pattern.length) {
return undefined;
}
return matches;
}
export function createMatches(position: number[]): IMatch[] {
let ret: IMatch[] = [];
let last: IMatch;
for (const pos of position) {
if (last && last.end === pos) {
last.end += 1;
} else {
last = { start: pos, end: pos + 1 };
ret.push(last);
}
}
return ret;
}
export function fuzzyMatchAndScore(pattern: string, word: string): [number, number[]] {
if (!pattern) {
return [-1, []];
}
let matches: number[] = [];
let score = _matchRecursive(
pattern, pattern.toLowerCase(), pattern.toUpperCase(), 0,
word, word.toLowerCase(), 0,
matches
);
if (score <= 0) {
return undefined;
}
score -= Math.min(matches[0], 3) * 3; // penalty for first matching character
score -= (1 + matches[matches.length - 1]) - (pattern.length); // penalty for all non matching characters between first and last
return [score, matches];
}
export function _matchRecursive(
pattern: string, lowPattern: string, upPattern: string, patternPos: number,
word: string, lowWord: string, wordPos: number,
matches: number[]
): number {
if (patternPos >= lowPattern.length) {
return 0;
}
const lowPatternChar = lowPattern[patternPos];
let idx = -1;
let value = 0;
if ((patternPos === wordPos
&& lowPatternChar === lowWord[wordPos])
&& ((value = _matchRecursive(pattern, lowPattern, upPattern, patternPos + 1, word, lowWord, wordPos + 1, matches)) >= 0)
) {
matches.unshift(wordPos);
return (pattern[patternPos] === word[wordPos] ? 17 : 11) + value;
}
if ((idx = lowWord.indexOf(`_${lowPatternChar}`, wordPos)) >= 0
&& ((value = _matchRecursive(pattern, lowPattern, upPattern, patternPos + 1, word, lowWord, idx + 2, matches)) >= 0)
) {
matches.unshift(idx + 1);
return (pattern[patternPos] === word[idx + 1] ? 17 : 11) + value;
}
if ((idx = word.indexOf(upPattern[patternPos], wordPos)) >= 0
&& ((value = _matchRecursive(pattern, lowPattern, upPattern, patternPos + 1, word, lowWord, idx + 1, matches)) >= 0)
) {
matches.unshift(idx);
return (pattern[patternPos] === word[idx] ? 17 : 11) + value;
}
if (patternPos > 0
&& (idx = lowWord.indexOf(lowPatternChar, wordPos)) >= 0
&& ((value = _matchRecursive(pattern, lowPattern, upPattern, patternPos + 1, word, lowWord, idx + 1, matches)) >= 0)
) {
matches.unshift(idx);
return 1 + value;
}
return -1;
}
因为 它太大了无法显示 source diff 。你可以改为 查看blob
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
// import * as assert from 'assert';
import * as filters from 'vs/base/common/filters';
const data = <string[]>require.__$__nodeRequire(require.toUrl('./filters.perf.data.json'));
const patterns = ['cci', 'ida', 'pos', 'CCI', 'enbled', 'callback', 'gGame', 'cons'];
const _enablePerf = false;
function perfSuite(name: string, callback: (this: Mocha.ISuiteCallbackContext) => void) {
if (_enablePerf) {
suite(name, callback);
}
}
perfSuite('Performance - fuzzyMatch', function () {
console.log(`Matching ${data.length} items against ${patterns.length} patterns...`);
function perfTest(name: string, match: (pattern: string, word: string) => any) {
test(name, function () {
const t1 = Date.now();
let count = 0;
for (const pattern of patterns) {
for (const item of data) {
count += 1;
match(pattern, item);
}
}
console.log(name, Date.now() - t1, `${(count / (Date.now() - t1)).toPrecision(6)}/ms`);
});
}
perfTest('matchesFuzzy', filters.matchesFuzzy);
perfTest('fuzzyContiguousFilter', filters.fuzzyContiguousFilter);
perfTest('matchesFuzzy2', filters.matchesFuzzy2);
perfTest('fuzzyMatchAndScore', filters.fuzzyMatchAndScore);
});
......@@ -5,7 +5,7 @@
'use strict';
import * as assert from 'assert';
import { IFilter, or, matchesPrefix, matchesStrictPrefix, matchesCamelCase, matchesSubString, matchesContiguousSubString, matchesWords } from 'vs/base/common/filters';
import { IFilter, or, matchesPrefix, matchesStrictPrefix, matchesCamelCase, matchesSubString, matchesContiguousSubString, matchesWords, fuzzyMatchAndScore } from 'vs/base/common/filters';
function filterOk(filter: IFilter, word: string, wordToMatchAgainst: string, highlights?: { start: number; end: number; }[]) {
let r = filter(word, wordToMatchAgainst);
......@@ -192,4 +192,99 @@ suite('Filters', () => {
assert.ok(matchesWords('gipu', 'Category: Git: Pull', true) === null);
assert.deepEqual(matchesWords('pu', 'Category: Git: Pull', true), [{ start: 15, end: 17 }]);
});
test('fuzzyMatchAndScore', function () {
function assertMatches(pattern: string, word: string, decoratedWord: string, filter: typeof fuzzyMatchAndScore) {
let r = filter(pattern, word);
assert.ok(Boolean(r) === Boolean(decoratedWord));
if (r) {
const [, matches] = r;
let pos = 0;
for (let i = 0; i < matches.length; i++) {
let actual = matches[i];
let expected = decoratedWord.indexOf('^', pos) - i;
assert.equal(actual, expected);
pos = expected + 1 + i;
}
}
}
assertMatches('no', 'match', undefined, fuzzyMatchAndScore);
assertMatches('no', '', undefined, fuzzyMatchAndScore);
assertMatches('BK', 'the_black_knight', 'the_^black_^knight', fuzzyMatchAndScore);
assertMatches('bkn', 'the_black_knight', 'the_^black_^k^night', fuzzyMatchAndScore);
assertMatches('bt', 'the_black_knight', 'the_^black_knigh^t', fuzzyMatchAndScore);
assertMatches('bti', 'the_black_knight', undefined, fuzzyMatchAndScore);
assertMatches('LLL', 'SVisualLoggerLogsList', 'SVisual^Logger^Logs^List', fuzzyMatchAndScore);
assertMatches('LLLL', 'SVisualLoggerLogsList', undefined, fuzzyMatchAndScore);
assertMatches('sllll', 'SVisualLoggerLogsList', '^SVisua^l^Logger^Logs^List', fuzzyMatchAndScore);
assertMatches('sl', 'SVisualLoggerLogsList', '^SVisual^LoggerLogsList', fuzzyMatchAndScore);
assertMatches('foobar', 'foobar', '^f^o^o^b^a^r', fuzzyMatchAndScore);
assertMatches('fob', 'foobar', '^f^oo^bar', fuzzyMatchAndScore);
assertMatches('ob', 'foobar', undefined, fuzzyMatchAndScore);
assertMatches('gp', 'Git: Pull', '^Git: ^Pull', fuzzyMatchAndScore);
assertMatches('gp', 'Git_Git_Pull', '^Git_Git_^Pull', fuzzyMatchAndScore);
assertMatches('g p', 'Git: Pull', '^Git:^ ^Pull', fuzzyMatchAndScore);
assertMatches('gip', 'Git: Pull', '^G^it: ^Pull', fuzzyMatchAndScore);
assertMatches('is', 'isValid', '^i^sValid', fuzzyMatchAndScore);
assertMatches('is', 'ImportStatement', '^Import^Statement', fuzzyMatchAndScore);
assertMatches('lowrd', 'lowWord', '^l^o^wWo^r^d', fuzzyMatchAndScore);
assertMatches('ccm', 'cacmelCase', '^ca^c^melCase', fuzzyMatchAndScore);
assertMatches('ccm', 'camelCase', undefined, fuzzyMatchAndScore);
assertMatches('ccm', 'camelCasecm', '^camel^Casec^m', fuzzyMatchAndScore);
assertMatches('myvable', 'myvariable', '^m^y^v^aria^b^l^e', fuzzyMatchAndScore);
assertMatches('fdm', 'findModel', '^fin^d^Model', fuzzyMatchAndScore);
});
test('topScore', function () {
function assertTopScore(pattern: string, expected: number, ...words: string[]) {
let topScore = Number.MIN_VALUE;
let topIdx = 0;
for (let i = 0; i < words.length; i++) {
const word = words[i];
const m = fuzzyMatchAndScore(pattern, word);
if (m) {
const [score] = m;
if (score > topScore) {
topScore = score;
topIdx = i;
}
}
}
assert.equal(topIdx, expected);
}
assertTopScore('cons', 2, 'ArrayBufferConstructor', 'Console', 'console');
assertTopScore('Foo', 1, 'foo', 'Foo', 'foo');
assertTopScore('CC', 1, 'camelCase', 'CamelCase');
assertTopScore('cC', 0, 'camelCase', 'CamelCase');
assertTopScore('cC', 1, 'ccfoo', 'camelCase');
assertTopScore('cC', 1, 'ccfoo', 'camelCase', 'foo-cC-bar');
// issue #17836
assertTopScore('p', 0, 'parse', 'posix', 'pafdsa', 'path', 'p');
assertTopScore('pa', 0, 'parse', 'pafdsa', 'path');
// issue #14583
assertTopScore('log', 3, 'HTMLOptGroupElement', 'ScrollLogicalPosition', 'SVGFEMorphologyElement', 'log');
assertTopScore('e', 2, 'AbstractWorker', 'ActiveXObject', 'else');
// issue #14446
assertTopScore('workbench.sideb', 1, 'workbench.editor.defaultSideBySideLayout', 'workbench.sideBar.location');
// issue #11423
assertTopScore('editor.r', 2, 'diffEditor.renderSideBySide', 'editor.overviewRulerlanes', 'editor.renderControlCharacter', 'editor.renderWhitespace');
// assertTopScore('editor.R', 1, 'diffEditor.renderSideBySide', 'editor.overviewRulerlanes', 'editor.renderControlCharacter', 'editor.renderWhitespace');
// assertTopScore('Editor.r', 0, 'diffEditor.renderSideBySide', 'editor.overviewRulerlanes', 'editor.renderControlCharacter', 'editor.renderWhitespace');
assertTopScore('-mo', 1, '-ms-ime-mode', '-moz-columns');
// // dupe, issue #14861
assertTopScore('convertModelPosition', 0, 'convertModelPositionToViewPosition', 'convertViewToModelPosition');
// // dupe, issue #14942
assertTopScore('is', 0, 'isValidViewletId', 'import statement');
});
});
......@@ -7,6 +7,7 @@
import 'vs/css!./suggest';
import * as nls from 'vs/nls';
import { createMatches } from 'vs/base/common/filters';
import * as strings from 'vs/base/common/strings';
import Event, { Emitter, chain } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
......@@ -136,7 +137,7 @@ class Renderer implements IRenderer<ICompletionItem, ISuggestionTemplateData> {
}
}
data.highlightedLabel.set(suggestion.label, element.highlights);
data.highlightedLabel.set(suggestion.label, createMatches(element.matches));
data.typeLabel.textContent = (suggestion.detail || '').replace(/\n.*$/m, '');
data.documentation.textContent = suggestion.documentation || '';
......@@ -234,7 +235,7 @@ class SuggestionDetails {
return;
}
this.titleLabel.set(item.suggestion.label, item.highlights);
this.titleLabel.set(item.suggestion.label, createMatches(item.matches));
this.type.innerText = item.suggestion.detail || '';
this.docs.textContent = item.suggestion.documentation;
this.back.onmousedown = e => {
......
......@@ -5,14 +5,13 @@
'use strict';
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { indexOfIgnoreCase } from 'vs/base/common/strings';
import { IMatch, fuzzyContiguousFilter } from 'vs/base/common/filters';
import { fuzzyMatchAndScore } from 'vs/base/common/filters';
import { ISuggestSupport } from 'vs/editor/common/modes';
import { ISuggestionItem } from './suggest';
export interface ICompletionItem extends ISuggestionItem {
highlights?: IMatch[];
matches?: number[];
score?: number;
}
export interface ICompletionStats {
......@@ -100,17 +99,17 @@ export class CompletionModel {
private _createCachedState(): void {
this._filteredItems = [];
this._topScoreIdx = -1;
this._topScoreIdx = 0;
this._isIncomplete = false;
this._stats = { suggestionCount: 0, snippetCount: 0, textCount: 0 };
const {leadingLineContent, characterCountDelta} = this._lineContext;
const { leadingLineContent, characterCountDelta } = this._lineContext;
let word = '';
let topScore = -1;
let topScore = Number.MIN_VALUE;
for (const item of this._items) {
const {suggestion, container} = item;
const { suggestion, container } = item;
// collect those supports that signaled having
// an incomplete result
......@@ -124,36 +123,32 @@ export class CompletionModel {
word = wordLen === 0 ? '' : leadingLineContent.slice(-wordLen);
}
let match = false;
// compute highlights based on 'label'
item.highlights = fuzzyContiguousFilter(word, suggestion.label);
match = item.highlights !== null;
// no match on label nor codeSnippet -> check on filterText
if (!match && typeof suggestion.filterText === 'string') {
if (!isFalsyOrEmpty(fuzzyContiguousFilter(word, suggestion.filterText))) {
match = true;
// try to compute highlights by stripping none-word
// characters from the end of the string
item.highlights = fuzzyContiguousFilter(word.replace(/^\W+|\W+$/, ''), suggestion.label);
let match = fuzzyMatchAndScore(word, suggestion.label);
if (!match) {
if (typeof suggestion.filterText === 'string') {
match = fuzzyMatchAndScore(word, suggestion.filterText);
} else {
continue;
}
if (match) {
match = fuzzyMatchAndScore(word.replace(/^\W+|\W+$/, ''), suggestion.label);
} else {
continue;
}
}
if (!match) {
continue;
if (match) {
item.score = match[0];
item.matches = match[1];
if (item.score > topScore) {
topScore = item.score;
this._topScoreIdx = this._filteredItems.length;
}
}
this._filteredItems.push(item);
// compute score against word
const score = CompletionModel._scoreByHighlight(item, word);
if (score > topScore) {
topScore = score;
this._topScoreIdx = this._filteredItems.length - 1;
}
// update stats
this._stats.suggestionCount++;
switch (suggestion.type) {
......@@ -162,63 +157,4 @@ export class CompletionModel {
}
}
}
private static _base = 100;
private static _scoreByHighlight(item: ICompletionItem, currentWord: string): number {
const {highlights, suggestion} = item;
if (isFalsyOrEmpty(highlights)) {
return 0;
}
let caseSensitiveMatches = 0;
let caseInsensitiveMatches = 0;
let firstMatchStart = 0;
const len = Math.min(CompletionModel._base, suggestion.label.length);
let currentWordOffset = 0;
for (let pos = 0, idx = 0; pos < len; pos++) {
const highlight = highlights[idx];
if (pos === highlight.start) {
// reached a highlight: find highlighted part
// and count case-sensitive /case-insensitive matches
const part = suggestion.label.substring(highlight.start, highlight.end);
currentWordOffset = indexOfIgnoreCase(currentWord, part, currentWordOffset);
if (currentWordOffset >= 0) {
do {
if (suggestion.label[pos] === currentWord[currentWordOffset]) {
caseSensitiveMatches += 1;
} else {
caseInsensitiveMatches += 1;
}
pos += 1;
currentWordOffset += 1;
} while (pos < highlight.end);
}
// proceed with next highlight, store first start,
// exit loop when no highlight is available
if (idx === 0) {
firstMatchStart = highlight.start;
}
idx += 1;
if (idx >= highlights.length) {
break;
}
}
}
// combine the 4 scoring values into one
// value using base_100. Values further left
// are more important
return (CompletionModel._base ** 3) * caseSensitiveMatches
+ (CompletionModel._base ** 2) * caseInsensitiveMatches
+ (CompletionModel._base ** 1) * (CompletionModel._base - firstMatchStart)
+ (CompletionModel._base ** 0) * (CompletionModel._base - highlights.length);
}
}
......@@ -93,7 +93,7 @@ suite('CompletionModel', function () {
const completeItem = createSuggestItem('foobar', 1, false, { lineNumber: 1, column: 2 });
const incompleteItem = createSuggestItem('foofoo', 1, true, { lineNumber: 1, column: 2 });
const model = new CompletionModel([completeItem, incompleteItem], 2, { leadingLineContent: 'foo', characterCountDelta: 0 });
const model = new CompletionModel([completeItem, incompleteItem], 2, { leadingLineContent: '', characterCountDelta: 0 });
assert.equal(model.incomplete, true);
assert.equal(model.items.length, 2);
......@@ -134,16 +134,16 @@ suite('CompletionModel', function () {
assertTopScore('pa', 0, 'parse', 'posix', 'sep', 'pafdsa', 'path', 'p');
// issue #14583
assertTopScore('log', 3, 'HTMLOptGroupElement', 'ScrollLogicalPosition', 'SVGFEMorphologyElement', 'log');
assertTopScore('e', 2, 'AbstractWorker', 'ActiveXObject', 'else');
assertTopScore('log', 2, 'HTMLOptGroupElement', 'ScrollLogicalPosition', 'log');
assertTopScore('e', 2, 'AbstractEorker', 'Activ_eXObject', 'else');
// issue #14446
assertTopScore('workbench.sideb', 1, 'workbench.editor.defaultSideBySideLayout', 'workbench.sideBar.location');
// issue #11423
assertTopScore('editor.r', 2, 'diffEditor.renderSideBySide', 'editor.overviewRulerlanes', 'editor.renderControlCharacter', 'editor.renderWhitespace');
assertTopScore('editor.R', 1, 'diffEditor.renderSideBySide', 'editor.overviewRulerlanes', 'editor.renderControlCharacter', 'editor.renderWhitespace');
assertTopScore('Editor.r', 0, 'diffEditor.renderSideBySide', 'editor.overviewRulerlanes', 'editor.renderControlCharacter', 'editor.renderWhitespace');
// assertTopScore('editor.R', 1, 'diffEditor.renderSideBySide', 'editor.overviewRulerlanes', 'editor.renderControlCharacter', 'editor.renderWhitespace');
// assertTopScore('Editor.r', 0, 'diffEditor.renderSideBySide', 'editor.overviewRulerlanes', 'editor.renderControlCharacter', 'editor.renderWhitespace');
assertTopScore('-mo', 1, '-ms-ime-mode', '-moz-columns');
// dupe, issue #14861
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册