diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index f8e2ee9b0366488260d50deae467a9177f19a1c2..6ae5bba6be9cafa7e38f61748349824ffe9fb857 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -383,3 +383,90 @@ export function matchesFuzzy2(pattern: string, word: string): IMatch[] | undefin return result.length > 0 ? result : undefined; } + +export function matchesFuzzy3(pattern: string, word: string) { + return _doMatchesFuzzy3( + pattern, pattern.toLowerCase(), 0, + word, word.toLowerCase(), 0, + [], 0 + ); +} +function _doMatchesFuzzy3( + pattern: string, lowPattern: string, patternPos: number, + word: string, lowWord: string, wordPos: number, + positions: number[], score: number +): [IMatch[], number] { + + let retryPoints: number[] = []; + let lastDidMatch = false; + + while (patternPos < lowPattern.length && wordPos < lowWord.length) { + const charLowPattern = lowPattern.charAt(patternPos); + const charLowWord = lowWord.charAt(wordPos); + + if (charLowPattern === charLowWord) { + + if (positions.length === 0) { + score = -Math.min(wordPos, 3) * 3; // penalty -> gaps at start + } else if (lastDidMatch) { + score += 1; // bonus -> subsequent match + } + + if (charLowWord !== word.charAt(wordPos)) { + score += 10; // bonus -> upper-case + + } else if (wordPos > 0 && word.charAt(wordPos).match(_separator)) { + score += 10; // bonus -> after a separator + + } else { + // keep this as a retry point + retryPoints.push(patternPos, wordPos + 1, positions.length, score); + } + + patternPos += 1; + positions.push(wordPos); + lastDidMatch = true; + + } else { + lastDidMatch = false; + score -= 1; // penalty -> gaps in match + } + + wordPos += 1; + } + + if (positions.length === 0) { + return undefined; + } + + const matches: IMatch[] = []; + let lastMatch: IMatch; + for (const pos of positions) { + if (lastMatch && lastMatch.end === pos) { + lastMatch.end += 1; + } else { + lastMatch = { start: pos, end: pos + 1 }; + matches.push(lastMatch); + } + } + + let result: [IMatch[], number] = [matches, score]; + + // try alternative matches + for (let i = 0; i < retryPoints.length; i += 4) { + const alt = _doMatchesFuzzy3( + pattern, lowPattern, retryPoints[i], + word, lowWord, retryPoints[i + 1], + positions.slice(0, retryPoints[i + 2]), retryPoints[i + 3] + ); + if (alt && alt[1] > result[1]) { + result = alt; + } + } + + return result; +} + +const _separator = /[-_. ]/; + + diff --git a/src/vs/base/test/common/filters.perf.test.ts b/src/vs/base/test/common/filters.perf.test.ts index 1e1ce6451eb1bd7d7f9c5f29f3f9a213589e84ca..c13069da571ea18159c6091aa14a6bc3f23797e1 100644 --- a/src/vs/base/test/common/filters.perf.test.ts +++ b/src/vs/base/test/common/filters.perf.test.ts @@ -5,10 +5,11 @@ 'use strict'; import * as assert from 'assert'; -import { fuzzyContiguousFilter, matchesFuzzy2 } from 'vs/base/common/filters'; +import { fuzzyContiguousFilter, matchesFuzzy2, matchesFuzzy3 } from 'vs/base/common/filters'; +const fuzz = require.__$__nodeRequire('fuzzaldrin-plus'); const data = <{ label: string }[]>require.__$__nodeRequire(require.toUrl('./filters.perf.data.json')); -const patterns = ['cci', 'CCI', 'ida', 'pos', 'enbled', 'callback', 'gGame']; +const patterns = ['cci', 'ida', 'pos', 'CCI', 'enbled', 'callback', 'gGame']; const _enablePerf = true; @@ -35,7 +36,7 @@ perfSuite('Performance - fuzzyMatch', function () { } } } - console.log(Date.now() - t1, count); + console.log(Date.now() - t1, count, (count / (Date.now() - t1)).toPrecision(5)); assert.ok(count > 0); }); @@ -52,7 +53,42 @@ perfSuite('Performance - fuzzyMatch', function () { } } } - console.log(Date.now() - t1, count); + console.log(Date.now() - t1, count, (count / (Date.now() - t1)).toPrecision(5)); + assert.ok(count > 0); + }); + + test('matchesFuzzy3', function () { + const t1 = Date.now(); + let count = 0; + for (const pattern of patterns) { + for (const item of data) { + if (item.label) { + const matches = matchesFuzzy3(pattern, item.label); + if (matches) { + count += 1; + } + } + } + } + console.log(Date.now() - t1, count, (count / (Date.now() - t1)).toPrecision(5)); + assert.ok(count > 0); + }); + + test('fuzzaldrin', function () { + const t1 = Date.now(); + let count = 0; + for (const pattern of patterns) { + for (const item of data) { + if (item.label) { + const matches = fuzz.match(item.label, pattern); + // fuzz.score(item.label, pattern); + if (matches) { + count += 1; + } + } + } + } + console.log(Date.now() - t1, count, (count / (Date.now() - t1)).toPrecision(5)); assert.ok(count > 0); }); }); diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index 2702b7ffd2df3897552494d1a066c58716c9f7e4..2e78f0857e740ddc371a2d4ad7195de647b7eb5e 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -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, matchesFuzzy3 } 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,36 @@ 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 toString(word: string, matches: IMatch[], score: number) { + // let underline = ''; + // let lastEnd = 0; + // for (const { start, end } of matches) { + // underline += new Array(1 + start - lastEnd).join(' '); + // underline += new Array(1 + end - start).join('^'); + // lastEnd = end; + // } + // return `${word} (score: ${score})\n${underline}`; + // } + + let [matches] = matchesFuzzy3('LLL', 'SVisualLoggerLogsList'); + assert.deepEqual(matches, [{ start: 7, end: 8 }, { start: 13, end: 14 }, { start: 17, end: 18 }]); + + [matches] = matchesFuzzy3('foobar', 'foobar'); + assert.deepEqual(matches, [{ start: 0, end: 6 }]); + + [matches] = matchesFuzzy3('tk', 'The Black Knight'); + assert.deepEqual(matches, [{ start: 0, end: 1 }, { start: 10, end: 11 }]); + + [matches] = matchesFuzzy3('Cat', 'charAt'); + assert.deepEqual(matches, [{ start: 0, end: 1 }, { start: 4, end: 6 }]); + + [matches] = matchesFuzzy3('Cat', 'charCodeAt'); + assert.deepEqual(matches, [{ start: 4, end: 5 }, { start: 8, end: 10 }]); + + [matches] = matchesFuzzy3('Cat', 'concat'); + assert.deepEqual(matches, [{ start: 0, end: 1 }, { start: 4, end: 6 }]); + + }); });