diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index e2067cdd02e901ee0a9538022d2f59279de5f0c9..9f85a2e34d024eac8cd5286e5abb7d11ca65538b 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -353,44 +353,50 @@ export function anyScore(pattern: string, lowPattern: string, _patternPos: numbe if (result) { return result; } - const matches: number[] = []; - let idx = 0; - for (let pos = 0; pos < lowPattern.length; ++pos) { - const thisIdx = lowWord.indexOf(lowPattern.charAt(pos), idx); - if (thisIdx >= 0) { - matches.push(thisIdx); - idx = thisIdx + 1; + let matches = 0; + let score = 0; + let idx = _wordPos; + for (let patternPos = 0; patternPos < lowPattern.length && patternPos < _maxLen; ++patternPos) { + const wordPos = lowWord.indexOf(lowPattern.charAt(patternPos), idx); + if (wordPos >= 0) { + score += 1; + matches += 2 ** wordPos; + idx = wordPos + 1; } } - return [matches.length, matches]; + return [score, matches, _wordPos]; } //#region --- fuzzyScore --- -export function createMatches(offsetOrScore: undefined | number[] | FuzzyScore): IMatch[] { - let ret: IMatch[] = []; - if (!offsetOrScore) { - return ret; - } - let offsets: number[]; - if (Array.isArray(offsetOrScore[1])) { - offsets = (offsetOrScore as FuzzyScore)[1]; - } else { - offsets = offsetOrScore as number[]; +export function createMatches(score: undefined | FuzzyScore): IMatch[] { + if (typeof score === 'undefined') { + return []; } - 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); + + const [, matches, wordStart] = score; + const res: IMatch[] = []; + + for (let pos = wordStart; pos < _masks.length; pos++) { + const mask = _masks[pos]; + if (mask > matches) { + break; + } else if (matches & mask) { + res.push({ start: pos, end: pos + 1 }); } } - return ret; + return res; } -const _maxLen = 100; +const _maxLen = 53; + +const _masks = (function () { + const result: number[] = []; + for (let pos = 0; pos < _maxLen; pos++) { + result.push(2 ** pos); + } + return result; +}()); function initTable() { const table: number[][] = []; @@ -478,7 +484,20 @@ function isPatternInWord(patternLow: string, patternPos: number, patternLen: num const enum Arrow { Top = 0b1, Diag = 0b10, Left = 0b100 } -export type FuzzyScore = [number, number[]]; +/** + * A tuple of three values. + * 0. the score + * 1. the matches encoded as bitmask (2^53) + * 2. the offset at which matching started + */ +export type FuzzyScore = [number, number, number]; + +export namespace FuzzyScore { + /** + * No matches and value `-100` + */ + export const Default: [-100, 0, 0] = [-100, 0, 0]; +} export interface FuzzyScorer { (pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, firstMatchCanBeWeak: boolean): FuzzyScore | undefined; @@ -501,6 +520,7 @@ export function fuzzyScore(pattern: string, patternLow: string, patternPos: numb } const patternStartPos = patternPos; + const wordStartPos = wordPos; // There will be a mach, fill in tables for (patternPos = patternStartPos + 1; patternPos <= patternLen; patternPos++) { @@ -574,29 +594,26 @@ export function fuzzyScore(pattern: string, patternLow: string, patternPos: numb console.log(printTable(_scores, pattern, patternLen, word, wordLen)); } - // _bucket is an array of [PrefixArray] we use to keep - // track of scores and matches. After calling `_findAllMatches` - // the best match (if available) is the first item in the array _matchesCount = 0; _topScore = -100; _patternStartPos = patternStartPos; _firstMatchCanBeWeak = firstMatchCanBeWeak; - _findAllMatches(patternLen, wordLen, patternLen === wordLen ? 1 : 0, new LazyArray(), false); - + _findAllMatches2(patternLen, wordLen, patternLen === wordLen ? 1 : 0, 0, false); if (_matchesCount === 0) { return undefined; } - return [_topScore, _topMatch.toArray()]; + return [_topScore, _topMatch2, wordStartPos]; } + let _matchesCount: number = 0; -let _topMatch: LazyArray; +let _topMatch2: number = 0; let _topScore: number = 0; let _patternStartPos: number = 0; let _firstMatchCanBeWeak: boolean = false; -function _findAllMatches(patternPos: number, wordPos: number, total: number, matches: LazyArray, lastMatched: boolean): void { +function _findAllMatches2(patternPos: number, wordPos: number, total: number, matches: number, lastMatched: boolean): void { if (_matchesCount >= 10 || total < -25) { // stop when having already 10 results, or @@ -612,11 +629,11 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat let arrow = _arrows[patternPos][wordPos]; if (arrow === Arrow.Left) { - // left + // left -> no match, skip a word character wordPos -= 1; if (lastMatched) { total -= 5; // new gap penalty - } else if (!matches.isEmpty()) { + } else if (matches !== 0) { total -= 1; // gap penalty after first match } lastMatched = false; @@ -626,11 +643,11 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat if (arrow & Arrow.Left) { // left - _findAllMatches( + _findAllMatches2( patternPos, wordPos - 1, - !matches.isEmpty() ? total - 1 : total, // gap penalty after first match - matches.slice(), + matches !== 0 ? total - 1 : total, // gap penalty after first match + matches, lastMatched ); } @@ -639,9 +656,11 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat total += score; patternPos -= 1; wordPos -= 1; - matches.unshift(wordPos); lastMatched = true; + // match -> set a 1 at the word pos + matches += 2 ** wordPos; + // count simple matches and boost a row of // simple matches when they yield in a // strong match. @@ -672,47 +691,7 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat _matchesCount += 1; if (total > _topScore) { _topScore = total; - _topMatch = matches; - } -} - -class LazyArray { - - private _parent: LazyArray; - private _parentLen: number; - private _data: number[]; - - isEmpty(): boolean { - return !this._data && (!this._parent || this._parent.isEmpty()); - } - - unshift(n: number) { - if (!this._data) { - this._data = [n]; - } else { - this._data.unshift(n); - } - } - - slice(): LazyArray { - const ret = new LazyArray(); - ret._parent = this; - ret._parentLen = this._data ? this._data.length : 0; return ret; - } - - toArray(): number[] { - if (!this._data) { - return this._parent.toArray(); - } - const bucket: number[][] = []; - let element = this; - while (element) { - if (element._parent && element._parent._data) { - bucket.push(element._parent._data.slice(element._parent._data.length - element._parentLen)); - } - element = element._parent; - } - return Array.prototype.concat.apply(this._data, bucket); + _topMatch2 = matches; } } diff --git a/src/vs/base/parts/quickopen/common/quickOpenScorer.ts b/src/vs/base/parts/quickopen/common/quickOpenScorer.ts index bb2efd7ad41cd76fc009d56da9006b22e8c9699d..bc06a3801fe5b67c032c288a1cf5895f0fd524bd 100644 --- a/src/vs/base/parts/quickopen/common/quickOpenScorer.ts +++ b/src/vs/base/parts/quickopen/common/quickOpenScorer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { compareAnything } from 'vs/base/common/comparers'; -import { matchesPrefix, IMatch, createMatches, matchesCamelCase, isUpper } from 'vs/base/common/filters'; +import { matchesPrefix, IMatch, matchesCamelCase, isUpper } from 'vs/base/common/filters'; import { nativeSep } from 'vs/base/common/paths'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { stripWildcards, equalsIgnoreCase } from 'vs/base/common/strings'; @@ -349,6 +349,23 @@ export function scoreItem(item: T, query: IPreparedQuery, fuzzy: boolean, acc return itemScore; } +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 doScoreItem(label: string, description: string, path: string, query: IPreparedQuery, fuzzy: boolean): IItemScore { // 1.) treat identity matches on full path highest @@ -605,4 +622,4 @@ export function fallbackCompare(itemA: T, itemB: T, query: IPreparedQuery, ac // equal return 0; -} \ No newline at end of file +} diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index 7c9300d17b4233b05f06f38f4991f16594c4fbf2..d8f84b0aced8b70db34474c6b86f6c15d854941e 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -206,16 +206,17 @@ suite('Filters', () => { function assertMatches(pattern: string, word: string, decoratedWord: string | undefined, filter: FuzzyScorer, opts: { patternPos?: number, wordPos?: number, firstMatchCanBeWeak?: boolean } = {}) { let r = filter(pattern, pattern.toLowerCase(), opts.patternPos || 0, word, word.toLowerCase(), opts.wordPos || 0, opts.firstMatchCanBeWeak || false); - assert.ok(!decoratedWord === (!r || r[1].length === 0)); + assert.ok(!decoratedWord === !r); 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; + let [, matches] = r; + let actualWord = ''; + for (let pos = 0; pos < word.length; pos++) { + if (2 ** pos & matches) { + actualWord += '^'; + } + actualWord += word[pos]; } + assert.equal(actualWord, decoratedWord); } } diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index a6181f4336b5997c583b0759854b2b2082583c2b..c3c2473bf5ac0c1debbb32c1f510cb9442486900 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -88,7 +88,7 @@ export abstract class TreeElement { export class OutlineElement extends TreeElement { children: { [id: string]: OutlineElement; } = Object.create(null); - score: FuzzyScore = [0, []]; + score: FuzzyScore = FuzzyScore.Default; marker: { count: number, topSev: MarkerSeverity }; constructor( @@ -136,7 +136,7 @@ export class OutlineGroup extends TreeElement { item.score = pattern ? fuzzyScore(pattern, pattern.toLowerCase(), 0, item.symbol.name, item.symbol.name.toLowerCase(), 0, true) - : [-100, []]; + : FuzzyScore.Default; if (item.score && (!topMatch || item.score[0] > topMatch.score[0])) { topMatch = item; @@ -146,7 +146,7 @@ export class OutlineGroup extends TreeElement { topMatch = this._updateMatches(pattern, child, topMatch); if (!item.score && child.score) { // don't filter parents with unfiltered children - item.score = [-100, []]; + item.score = FuzzyScore.Default; } } return topMatch; diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/editor/contrib/documentSymbols/outlineTree.ts index 98b50239d44ca987d979930f5738b274de3686d2..3daabc1627d2aaeb397e64b66abb5bfe25d91936 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineTree.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineTree.ts @@ -161,7 +161,7 @@ export class OutlineRenderer implements IRenderer { renderElement(tree: ITree, element: OutlineGroup | OutlineElement, templateId: string, template: OutlineTemplate): void { if (element instanceof OutlineElement) { template.icon.className = `outline-element-icon ${symbolKindToCssClass(element.symbol.kind)}`; - template.label.set(element.symbol.name, element.score ? createMatches(element.score[1]) : undefined, localize('title.template', "{0} ({1})", element.symbol.name, OutlineRenderer._symbolKindNames[element.symbol.kind])); + template.label.set(element.symbol.name, element.score ? createMatches(element.score) : undefined, localize('title.template', "{0} ({1})", element.symbol.name, OutlineRenderer._symbolKindNames[element.symbol.kind])); template.detail.innerText = element.symbol.detail || ''; this._renderMarkerInfo(element, template); diff --git a/src/vs/editor/contrib/suggest/completionModel.ts b/src/vs/editor/contrib/suggest/completionModel.ts index 82ad963833517fbc0660a1005985e888e7d58010..9d23376cffd7bc39b0b3ec77184bc8312c25351e 100644 --- a/src/vs/editor/contrib/suggest/completionModel.ts +++ b/src/vs/editor/contrib/suggest/completionModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { fuzzyScore, fuzzyScoreGracefulAggressive, anyScore, FuzzyScorer } from 'vs/base/common/filters'; +import { fuzzyScore, fuzzyScoreGracefulAggressive, anyScore, FuzzyScorer, FuzzyScore } from 'vs/base/common/filters'; import { isDisposable } from 'vs/base/common/lifecycle'; import { CompletionList, CompletionItemProvider, CompletionItemKind } from 'vs/editor/common/modes'; import { CompletionItem } from './suggest'; @@ -186,8 +186,7 @@ export class CompletionModel { // the fallback-sort using the initial sort order. // use a score of `-100` because that is out of the // bound of values `fuzzyScore` will return - item.score = -100; - item.matches = []; + item.score = FuzzyScore.Default; } else { // skip word characters that are whitespace until @@ -205,8 +204,7 @@ export class CompletionModel { if (wordPos >= wordLen) { // the wordPos at which scoring starts is the whole word // and therefore the same rules as not having a word apply - item.score = -100; - item.matches = []; + item.score = FuzzyScore.Default; } else if (typeof item.completion.filterText === 'string') { // when there is a `filterText` it must match the `word`. @@ -217,8 +215,8 @@ export class CompletionModel { if (!match) { continue; // NO match } - item.score = match[0]; - item.matches = anyScore(word, wordLow, 0, item.completion.label, item.labelLow, 0)[1]; + item.score = anyScore(word, wordLow, 0, item.completion.label, item.labelLow, 0); + item.score[0] = match[0]; // use score from filterText } else { // by default match `word` against the `label` @@ -226,8 +224,7 @@ export class CompletionModel { if (!match) { continue; // NO match } - item.score = match[0]; - item.matches = match[1]; + item.score = match; } } @@ -248,9 +245,9 @@ export class CompletionModel { } private static _compareCompletionItems(a: StrictCompletionItem, b: StrictCompletionItem): number { - if (a.score > b.score) { + if (a.score[0] > b.score[0]) { return -1; - } else if (a.score < b.score) { + } else if (a.score[0] < b.score[0]) { return 1; } else if (a.distance < b.distance) { return -1; diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index 969e5cef5aaff33a531d873cbc3aec7b39f3a0a2..3d7e3b008cd07549eb33e005baca60a53d1ac89b 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -16,6 +16,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Range } from 'vs/editor/common/core/range'; +import { FuzzyScore } from 'vs/base/common/filters'; export const Context = { Visible: new RawContextKey('suggestWidgetVisible', false), @@ -36,9 +37,8 @@ export class CompletionItem { readonly filterTextLow?: string; // sorting, filtering - score: number = -100; + score: FuzzyScore = FuzzyScore.Default; distance: number = 0; - matches?: number[]; idx?: number; word?: string; diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 9aa201398dc5504a505425b2407b8f0cf9d69b6f..de7330e305ecca130d3ba7ab66c4ab4b1ecd0606 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -153,7 +153,7 @@ class Renderer implements IListRenderer const labelOptions: IIconLabelValueOptions = { labelEscapeNewLines: true, - matches: createMatches(element.matches) + matches: createMatches(element.score) }; let color: string;