提交 ff630f90 编写于 作者: J Johannes Rieken

filter - use a number instead of an arrayy to represent matches

上级 f58e44f5
...@@ -353,44 +353,50 @@ export function anyScore(pattern: string, lowPattern: string, _patternPos: numbe ...@@ -353,44 +353,50 @@ export function anyScore(pattern: string, lowPattern: string, _patternPos: numbe
if (result) { if (result) {
return result; return result;
} }
const matches: number[] = []; let matches = 0;
let idx = 0; let score = 0;
for (let pos = 0; pos < lowPattern.length; ++pos) { let idx = _wordPos;
const thisIdx = lowWord.indexOf(lowPattern.charAt(pos), idx); for (let patternPos = 0; patternPos < lowPattern.length && patternPos < _maxLen; ++patternPos) {
if (thisIdx >= 0) { const wordPos = lowWord.indexOf(lowPattern.charAt(patternPos), idx);
matches.push(thisIdx); if (wordPos >= 0) {
idx = thisIdx + 1; score += 1;
matches += 2 ** wordPos;
idx = wordPos + 1;
} }
} }
return [matches.length, matches]; return [score, matches, _wordPos];
} }
//#region --- fuzzyScore --- //#region --- fuzzyScore ---
export function createMatches(offsetOrScore: undefined | number[] | FuzzyScore): IMatch[] { export function createMatches(score: undefined | FuzzyScore): IMatch[] {
let ret: IMatch[] = []; if (typeof score === 'undefined') {
if (!offsetOrScore) { return [];
return ret;
}
let offsets: number[];
if (Array.isArray(offsetOrScore[1])) {
offsets = (offsetOrScore as FuzzyScore)[1];
} else {
offsets = offsetOrScore as number[];
} }
let last: IMatch | undefined;
for (const pos of offsets) { const [, matches, wordStart] = score;
if (last && last.end === pos) { const res: IMatch[] = [];
last.end += 1;
} else { for (let pos = wordStart; pos < _masks.length; pos++) {
last = { start: pos, end: pos + 1 }; const mask = _masks[pos];
ret.push(last); 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() { function initTable() {
const table: number[][] = []; const table: number[][] = [];
...@@ -478,7 +484,20 @@ function isPatternInWord(patternLow: string, patternPos: number, patternLen: num ...@@ -478,7 +484,20 @@ function isPatternInWord(patternLow: string, patternPos: number, patternLen: num
const enum Arrow { Top = 0b1, Diag = 0b10, Left = 0b100 } 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 { export interface FuzzyScorer {
(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, firstMatchCanBeWeak: boolean): FuzzyScore | undefined; (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 ...@@ -501,6 +520,7 @@ export function fuzzyScore(pattern: string, patternLow: string, patternPos: numb
} }
const patternStartPos = patternPos; const patternStartPos = patternPos;
const wordStartPos = wordPos;
// There will be a mach, fill in tables // There will be a mach, fill in tables
for (patternPos = patternStartPos + 1; patternPos <= patternLen; patternPos++) { for (patternPos = patternStartPos + 1; patternPos <= patternLen; patternPos++) {
...@@ -574,29 +594,26 @@ export function fuzzyScore(pattern: string, patternLow: string, patternPos: numb ...@@ -574,29 +594,26 @@ export function fuzzyScore(pattern: string, patternLow: string, patternPos: numb
console.log(printTable(_scores, pattern, patternLen, word, wordLen)); 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; _matchesCount = 0;
_topScore = -100; _topScore = -100;
_patternStartPos = patternStartPos; _patternStartPos = patternStartPos;
_firstMatchCanBeWeak = firstMatchCanBeWeak; _firstMatchCanBeWeak = firstMatchCanBeWeak;
_findAllMatches(patternLen, wordLen, patternLen === wordLen ? 1 : 0, new LazyArray(), false); _findAllMatches2(patternLen, wordLen, patternLen === wordLen ? 1 : 0, 0, false);
if (_matchesCount === 0) { if (_matchesCount === 0) {
return undefined; return undefined;
} }
return [_topScore, _topMatch.toArray()]; return [_topScore, _topMatch2, wordStartPos];
} }
let _matchesCount: number = 0; let _matchesCount: number = 0;
let _topMatch: LazyArray; let _topMatch2: number = 0;
let _topScore: number = 0; let _topScore: number = 0;
let _patternStartPos: number = 0; let _patternStartPos: number = 0;
let _firstMatchCanBeWeak: boolean = false; 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) { if (_matchesCount >= 10 || total < -25) {
// stop when having already 10 results, or // stop when having already 10 results, or
...@@ -612,11 +629,11 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat ...@@ -612,11 +629,11 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat
let arrow = _arrows[patternPos][wordPos]; let arrow = _arrows[patternPos][wordPos];
if (arrow === Arrow.Left) { if (arrow === Arrow.Left) {
// left // left -> no match, skip a word character
wordPos -= 1; wordPos -= 1;
if (lastMatched) { if (lastMatched) {
total -= 5; // new gap penalty total -= 5; // new gap penalty
} else if (!matches.isEmpty()) { } else if (matches !== 0) {
total -= 1; // gap penalty after first match total -= 1; // gap penalty after first match
} }
lastMatched = false; lastMatched = false;
...@@ -626,11 +643,11 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat ...@@ -626,11 +643,11 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat
if (arrow & Arrow.Left) { if (arrow & Arrow.Left) {
// left // left
_findAllMatches( _findAllMatches2(
patternPos, patternPos,
wordPos - 1, wordPos - 1,
!matches.isEmpty() ? total - 1 : total, // gap penalty after first match matches !== 0 ? total - 1 : total, // gap penalty after first match
matches.slice(), matches,
lastMatched lastMatched
); );
} }
...@@ -639,9 +656,11 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat ...@@ -639,9 +656,11 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat
total += score; total += score;
patternPos -= 1; patternPos -= 1;
wordPos -= 1; wordPos -= 1;
matches.unshift(wordPos);
lastMatched = true; lastMatched = true;
// match -> set a 1 at the word pos
matches += 2 ** wordPos;
// count simple matches and boost a row of // count simple matches and boost a row of
// simple matches when they yield in a // simple matches when they yield in a
// strong match. // strong match.
...@@ -672,47 +691,7 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat ...@@ -672,47 +691,7 @@ function _findAllMatches(patternPos: number, wordPos: number, total: number, mat
_matchesCount += 1; _matchesCount += 1;
if (total > _topScore) { if (total > _topScore) {
_topScore = total; _topScore = total;
_topMatch = matches; _topMatch2 = 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 = <LazyArray>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);
} }
} }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { compareAnything } from 'vs/base/common/comparers'; 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 { nativeSep } from 'vs/base/common/paths';
import { isWindows, isLinux } from 'vs/base/common/platform'; import { isWindows, isLinux } from 'vs/base/common/platform';
import { stripWildcards, equalsIgnoreCase } from 'vs/base/common/strings'; import { stripWildcards, equalsIgnoreCase } from 'vs/base/common/strings';
...@@ -349,6 +349,23 @@ export function scoreItem<T>(item: T, query: IPreparedQuery, fuzzy: boolean, acc ...@@ -349,6 +349,23 @@ export function scoreItem<T>(item: T, query: IPreparedQuery, fuzzy: boolean, acc
return itemScore; 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 { function doScoreItem(label: string, description: string, path: string, query: IPreparedQuery, fuzzy: boolean): IItemScore {
// 1.) treat identity matches on full path highest // 1.) treat identity matches on full path highest
...@@ -605,4 +622,4 @@ export function fallbackCompare<T>(itemA: T, itemB: T, query: IPreparedQuery, ac ...@@ -605,4 +622,4 @@ export function fallbackCompare<T>(itemA: T, itemB: T, query: IPreparedQuery, ac
// equal // equal
return 0; return 0;
} }
\ No newline at end of file
...@@ -206,16 +206,17 @@ suite('Filters', () => { ...@@ -206,16 +206,17 @@ suite('Filters', () => {
function assertMatches(pattern: string, word: string, decoratedWord: string | undefined, filter: FuzzyScorer, opts: { patternPos?: number, wordPos?: number, firstMatchCanBeWeak?: boolean } = {}) { 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); 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) { if (r) {
const [, matches] = r; let [, matches] = r;
let pos = 0; let actualWord = '';
for (let i = 0; i < matches.length; i++) { for (let pos = 0; pos < word.length; pos++) {
let actual = matches[i]; if (2 ** pos & matches) {
let expected = decoratedWord!.indexOf('^', pos) - i; actualWord += '^';
assert.equal(actual, expected); }
pos = expected + 1 + i; actualWord += word[pos];
} }
assert.equal(actualWord, decoratedWord);
} }
} }
......
...@@ -88,7 +88,7 @@ export abstract class TreeElement { ...@@ -88,7 +88,7 @@ export abstract class TreeElement {
export class OutlineElement extends TreeElement { export class OutlineElement extends TreeElement {
children: { [id: string]: OutlineElement; } = Object.create(null); children: { [id: string]: OutlineElement; } = Object.create(null);
score: FuzzyScore = [0, []]; score: FuzzyScore = FuzzyScore.Default;
marker: { count: number, topSev: MarkerSeverity }; marker: { count: number, topSev: MarkerSeverity };
constructor( constructor(
...@@ -136,7 +136,7 @@ export class OutlineGroup extends TreeElement { ...@@ -136,7 +136,7 @@ export class OutlineGroup extends TreeElement {
item.score = pattern item.score = pattern
? fuzzyScore(pattern, pattern.toLowerCase(), 0, item.symbol.name, item.symbol.name.toLowerCase(), 0, true) ? 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])) { if (item.score && (!topMatch || item.score[0] > topMatch.score[0])) {
topMatch = item; topMatch = item;
...@@ -146,7 +146,7 @@ export class OutlineGroup extends TreeElement { ...@@ -146,7 +146,7 @@ export class OutlineGroup extends TreeElement {
topMatch = this._updateMatches(pattern, child, topMatch); topMatch = this._updateMatches(pattern, child, topMatch);
if (!item.score && child.score) { if (!item.score && child.score) {
// don't filter parents with unfiltered children // don't filter parents with unfiltered children
item.score = [-100, []]; item.score = FuzzyScore.Default;
} }
} }
return topMatch; return topMatch;
......
...@@ -161,7 +161,7 @@ export class OutlineRenderer implements IRenderer { ...@@ -161,7 +161,7 @@ export class OutlineRenderer implements IRenderer {
renderElement(tree: ITree, element: OutlineGroup | OutlineElement, templateId: string, template: OutlineTemplate): void { renderElement(tree: ITree, element: OutlineGroup | OutlineElement, templateId: string, template: OutlineTemplate): void {
if (element instanceof OutlineElement) { if (element instanceof OutlineElement) {
template.icon.className = `outline-element-icon ${symbolKindToCssClass(element.symbol.kind)}`; 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 || ''; template.detail.innerText = element.symbol.detail || '';
this._renderMarkerInfo(element, template); this._renderMarkerInfo(element, template);
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * 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 { isDisposable } from 'vs/base/common/lifecycle';
import { CompletionList, CompletionItemProvider, CompletionItemKind } from 'vs/editor/common/modes'; import { CompletionList, CompletionItemProvider, CompletionItemKind } from 'vs/editor/common/modes';
import { CompletionItem } from './suggest'; import { CompletionItem } from './suggest';
...@@ -186,8 +186,7 @@ export class CompletionModel { ...@@ -186,8 +186,7 @@ export class CompletionModel {
// the fallback-sort using the initial sort order. // the fallback-sort using the initial sort order.
// use a score of `-100` because that is out of the // use a score of `-100` because that is out of the
// bound of values `fuzzyScore` will return // bound of values `fuzzyScore` will return
item.score = -100; item.score = FuzzyScore.Default;
item.matches = [];
} else { } else {
// skip word characters that are whitespace until // skip word characters that are whitespace until
...@@ -205,8 +204,7 @@ export class CompletionModel { ...@@ -205,8 +204,7 @@ export class CompletionModel {
if (wordPos >= wordLen) { if (wordPos >= wordLen) {
// the wordPos at which scoring starts is the whole word // the wordPos at which scoring starts is the whole word
// and therefore the same rules as not having a word apply // and therefore the same rules as not having a word apply
item.score = -100; item.score = FuzzyScore.Default;
item.matches = [];
} else if (typeof item.completion.filterText === 'string') { } else if (typeof item.completion.filterText === 'string') {
// when there is a `filterText` it must match the `word`. // when there is a `filterText` it must match the `word`.
...@@ -217,8 +215,8 @@ export class CompletionModel { ...@@ -217,8 +215,8 @@ export class CompletionModel {
if (!match) { if (!match) {
continue; // NO match continue; // NO match
} }
item.score = match[0]; item.score = anyScore(word, wordLow, 0, item.completion.label, item.labelLow, 0);
item.matches = anyScore(word, wordLow, 0, item.completion.label, item.labelLow, 0)[1]; item.score[0] = match[0]; // use score from filterText
} else { } else {
// by default match `word` against the `label` // by default match `word` against the `label`
...@@ -226,8 +224,7 @@ export class CompletionModel { ...@@ -226,8 +224,7 @@ export class CompletionModel {
if (!match) { if (!match) {
continue; // NO match continue; // NO match
} }
item.score = match[0]; item.score = match;
item.matches = match[1];
} }
} }
...@@ -248,9 +245,9 @@ export class CompletionModel { ...@@ -248,9 +245,9 @@ export class CompletionModel {
} }
private static _compareCompletionItems(a: StrictCompletionItem, b: StrictCompletionItem): number { private static _compareCompletionItems(a: StrictCompletionItem, b: StrictCompletionItem): number {
if (a.score > b.score) { if (a.score[0] > b.score[0]) {
return -1; return -1;
} else if (a.score < b.score) { } else if (a.score[0] < b.score[0]) {
return 1; return 1;
} else if (a.distance < b.distance) { } else if (a.distance < b.distance) {
return -1; return -1;
......
...@@ -16,6 +16,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; ...@@ -16,6 +16,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { Range } from 'vs/editor/common/core/range'; import { Range } from 'vs/editor/common/core/range';
import { FuzzyScore } from 'vs/base/common/filters';
export const Context = { export const Context = {
Visible: new RawContextKey<boolean>('suggestWidgetVisible', false), Visible: new RawContextKey<boolean>('suggestWidgetVisible', false),
...@@ -36,9 +37,8 @@ export class CompletionItem { ...@@ -36,9 +37,8 @@ export class CompletionItem {
readonly filterTextLow?: string; readonly filterTextLow?: string;
// sorting, filtering // sorting, filtering
score: number = -100; score: FuzzyScore = FuzzyScore.Default;
distance: number = 0; distance: number = 0;
matches?: number[];
idx?: number; idx?: number;
word?: string; word?: string;
......
...@@ -153,7 +153,7 @@ class Renderer implements IListRenderer<CompletionItem, ISuggestionTemplateData> ...@@ -153,7 +153,7 @@ class Renderer implements IListRenderer<CompletionItem, ISuggestionTemplateData>
const labelOptions: IIconLabelValueOptions = { const labelOptions: IIconLabelValueOptions = {
labelEscapeNewLines: true, labelEscapeNewLines: true,
matches: createMatches(element.matches) matches: createMatches(element.score)
}; };
let color: string; let color: string;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册