提交 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
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 = <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);
_topMatch2 = matches;
}
}
......
......@@ -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<T>(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<T>(itemA: T, itemB: T, query: IPreparedQuery, ac
// equal
return 0;
}
\ No newline at end of file
}
......@@ -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);
}
}
......
......@@ -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;
......
......@@ -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);
......
......@@ -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;
......
......@@ -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<boolean>('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;
......
......@@ -153,7 +153,7 @@ class Renderer implements IListRenderer<CompletionItem, ISuggestionTemplateData>
const labelOptions: IIconLabelValueOptions = {
labelEscapeNewLines: true,
matches: createMatches(element.matches)
matches: createMatches(element.score)
};
let color: string;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册