filters.ts 16.6 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

B
Benjamin Pasero 已提交
7
import strings = require('vs/base/common/strings');
J
Johannes Rieken 已提交
8 9
import { BoundedLinkedMap } from 'vs/base/common/map';
import { CharCode } from 'vs/base/common/charCode';
E
Erich Gamma 已提交
10 11 12

export interface IFilter {
	// Returns null if word doesn't match.
B
Benjamin Pasero 已提交
13
	(word: string, wordToMatchAgainst: string): IMatch[];
E
Erich Gamma 已提交
14 15 16
}

export interface IMatch {
B
Benjamin Pasero 已提交
17 18
	start: number;
	end: number;
E
Erich Gamma 已提交
19 20 21 22 23
}

// Combined filters

/**
24
 * @returns A filter which combines the provided set
E
Erich Gamma 已提交
25 26 27 28
 * of filters with an or. The *first* filters that
 * matches defined the return value of the returned
 * filter.
 */
B
Benjamin Pasero 已提交
29
export function or(...filter: IFilter[]): IFilter {
30
	return function (word: string, wordToMatchAgainst: string): IMatch[] {
B
Benjamin Pasero 已提交
31 32 33
		for (let i = 0, len = filter.length; i < len; i++) {
			let match = filter[i](word, wordToMatchAgainst);
			if (match) {
E
Erich Gamma 已提交
34 35 36 37 38 39 40 41
				return match;
			}
		}
		return null;
	};
}

/**
42
 * @returns A filter which combines the provided set
E
Erich Gamma 已提交
43 44 45
 * of filters with an and. The combines matches are
 * returned if *all* filters match.
 */
B
Benjamin Pasero 已提交
46
export function and(...filter: IFilter[]): IFilter {
47
	return function (word: string, wordToMatchAgainst: string): IMatch[] {
B
Benjamin Pasero 已提交
48 49 50 51
		let result: IMatch[] = [];
		for (let i = 0, len = filter.length; i < len; i++) {
			let match = filter[i](word, wordToMatchAgainst);
			if (!match) {
E
Erich Gamma 已提交
52 53 54 55 56 57 58 59 60 61
				return null;
			}
			result = result.concat(match);
		}
		return result;
	};
}

// Prefix

62 63
export const matchesStrictPrefix: IFilter = _matchesPrefix.bind(undefined, false);
export const matchesPrefix: IFilter = _matchesPrefix.bind(undefined, true);
E
Erich Gamma 已提交
64

B
Benjamin Pasero 已提交
65
function _matchesPrefix(ignoreCase: boolean, word: string, wordToMatchAgainst: string): IMatch[] {
66
	if (!wordToMatchAgainst || wordToMatchAgainst.length < word.length) {
E
Erich Gamma 已提交
67 68
		return null;
	}
69

70 71 72 73 74 75
	let matches: boolean;
	if (ignoreCase) {
		matches = strings.beginsWithIgnoreCase(wordToMatchAgainst, word);
	} else {
		matches = wordToMatchAgainst.indexOf(word) === 0;
	}
76

77
	if (!matches) {
78
		return null;
E
Erich Gamma 已提交
79
	}
80

E
Erich Gamma 已提交
81 82 83 84 85
	return word.length > 0 ? [{ start: 0, end: word.length }] : [];
}

// Contiguous Substring

B
Benjamin Pasero 已提交
86 87
export function matchesContiguousSubString(word: string, wordToMatchAgainst: string): IMatch[] {
	let index = wordToMatchAgainst.toLowerCase().indexOf(word.toLowerCase());
E
Erich Gamma 已提交
88 89 90 91 92 93 94 95 96
	if (index === -1) {
		return null;
	}

	return [{ start: index, end: index + word.length }];
}

// Substring

B
Benjamin Pasero 已提交
97
export function matchesSubString(word: string, wordToMatchAgainst: string): IMatch[] {
E
Erich Gamma 已提交
98 99 100
	return _matchesSubString(word.toLowerCase(), wordToMatchAgainst.toLowerCase(), 0, 0);
}

B
Benjamin Pasero 已提交
101
function _matchesSubString(word: string, wordToMatchAgainst: string, i: number, j: number): IMatch[] {
E
Erich Gamma 已提交
102 103 104 105 106 107
	if (i === word.length) {
		return [];
	} else if (j === wordToMatchAgainst.length) {
		return null;
	} else {
		if (word[i] === wordToMatchAgainst[j]) {
B
Benjamin Pasero 已提交
108
			let result: IMatch[] = null;
E
Erich Gamma 已提交
109 110 111 112 113 114 115 116 117 118 119
			if (result = _matchesSubString(word, wordToMatchAgainst, i + 1, j + 1)) {
				return join({ start: j, end: j + 1 }, result);
			}
		}

		return _matchesSubString(word, wordToMatchAgainst, i, j + 1);
	}
}

// CamelCase

B
Benjamin Pasero 已提交
120
function isLower(code: number): boolean {
121
	return CharCode.a <= code && code <= CharCode.z;
E
Erich Gamma 已提交
122 123
}

B
Benjamin Pasero 已提交
124
function isUpper(code: number): boolean {
125
	return CharCode.A <= code && code <= CharCode.Z;
E
Erich Gamma 已提交
126 127
}

B
Benjamin Pasero 已提交
128
function isNumber(code: number): boolean {
129
	return CharCode.Digit0 <= code && code <= CharCode.Digit9;
E
Erich Gamma 已提交
130 131
}

B
Benjamin Pasero 已提交
132
function isWhitespace(code: number): boolean {
133 134 135 136 137 138
	return (
		code === CharCode.Space
		|| code === CharCode.Tab
		|| code === CharCode.LineFeed
		|| code === CharCode.CarriageReturn
	);
E
Erich Gamma 已提交
139 140
}

B
Benjamin Pasero 已提交
141
function isAlphanumeric(code: number): boolean {
E
Erich Gamma 已提交
142 143 144
	return isLower(code) || isUpper(code) || isNumber(code);
}

B
Benjamin Pasero 已提交
145
function join(head: IMatch, tail: IMatch[]): IMatch[] {
E
Erich Gamma 已提交
146 147 148 149 150 151 152 153 154 155 156
	if (tail.length === 0) {
		tail = [head];
	} else if (head.end === tail[0].start) {
		tail[0].start = head.start;
	} else {
		tail.unshift(head);
	}
	return tail;
}

function nextAnchor(camelCaseWord: string, start: number): number {
B
Benjamin Pasero 已提交
157 158 159
	for (let i = start; i < camelCaseWord.length; i++) {
		let c = camelCaseWord.charCodeAt(i);
		if (isUpper(c) || isNumber(c) || (i > 0 && !isAlphanumeric(camelCaseWord.charCodeAt(i - 1)))) {
E
Erich Gamma 已提交
160 161 162 163 164 165
			return i;
		}
	}
	return camelCaseWord.length;
}

B
Benjamin Pasero 已提交
166
function _matchesCamelCase(word: string, camelCaseWord: string, i: number, j: number): IMatch[] {
E
Erich Gamma 已提交
167 168 169 170 171 172 173
	if (i === word.length) {
		return [];
	} else if (j === camelCaseWord.length) {
		return null;
	} else if (word[i] !== camelCaseWord[j].toLowerCase()) {
		return null;
	} else {
B
Benjamin Pasero 已提交
174
		let result: IMatch[] = null;
B
Benjamin Pasero 已提交
175
		let nextUpperIndex = j + 1;
E
Erich Gamma 已提交
176 177 178 179 180 181 182 183 184
		result = _matchesCamelCase(word, camelCaseWord, i + 1, j + 1);
		while (!result && (nextUpperIndex = nextAnchor(camelCaseWord, nextUpperIndex)) < camelCaseWord.length) {
			result = _matchesCamelCase(word, camelCaseWord, i + 1, nextUpperIndex);
			nextUpperIndex++;
		}
		return result === null ? null : join({ start: j, end: j + 1 }, result);
	}
}

J
Joao Moreno 已提交
185 186 187 188 189 190 191
interface ICamelCaseAnalysis {
	upperPercent: number;
	lowerPercent: number;
	alphaPercent: number;
	numericPercent: number;
}

E
Erich Gamma 已提交
192 193
// Heuristic to avoid computing camel case matcher for words that don't
// look like camelCaseWords.
J
Joao Moreno 已提交
194
function analyzeCamelCaseWord(word: string): ICamelCaseAnalysis {
195
	let upper = 0, lower = 0, alpha = 0, numeric = 0, code = 0;
E
Erich Gamma 已提交
196

B
Benjamin Pasero 已提交
197
	for (let i = 0; i < word.length; i++) {
E
Erich Gamma 已提交
198 199
		code = word.charCodeAt(i);

200 201 202 203
		if (isUpper(code)) { upper++; }
		if (isLower(code)) { lower++; }
		if (isAlphanumeric(code)) { alpha++; }
		if (isNumber(code)) { numeric++; }
E
Erich Gamma 已提交
204 205
	}

B
Benjamin Pasero 已提交
206 207 208
	let upperPercent = upper / word.length;
	let lowerPercent = lower / word.length;
	let alphaPercent = alpha / word.length;
209
	let numericPercent = numeric / word.length;
E
Erich Gamma 已提交
210

J
Joao Moreno 已提交
211 212 213 214
	return { upperPercent, lowerPercent, alphaPercent, numericPercent };
}

function isUpperCaseWord(analysis: ICamelCaseAnalysis): boolean {
B
Benjamin Pasero 已提交
215
	const { upperPercent, lowerPercent } = analysis;
J
Joao Moreno 已提交
216 217 218 219 220
	return lowerPercent === 0 && upperPercent > 0.6;
}

function isCamelCaseWord(analysis: ICamelCaseAnalysis): boolean {
	const { upperPercent, lowerPercent, alphaPercent, numericPercent } = analysis;
221
	return lowerPercent > 0.2 && upperPercent < 0.8 && alphaPercent > 0.6 && numericPercent < 0.2;
E
Erich Gamma 已提交
222 223 224 225 226
}

// Heuristic to avoid computing camel case matcher for words that don't
// look like camel case patterns.
function isCamelCasePattern(word: string): boolean {
B
Benjamin Pasero 已提交
227
	let upper = 0, lower = 0, code = 0, whitespace = 0;
E
Erich Gamma 已提交
228

B
Benjamin Pasero 已提交
229
	for (let i = 0; i < word.length; i++) {
E
Erich Gamma 已提交
230 231
		code = word.charCodeAt(i);

232 233 234
		if (isUpper(code)) { upper++; }
		if (isLower(code)) { lower++; }
		if (isWhitespace(code)) { whitespace++; }
E
Erich Gamma 已提交
235 236 237 238 239 240 241 242 243
	}

	if ((upper === 0 || lower === 0) && whitespace === 0) {
		return word.length <= 30;
	} else {
		return upper <= 5;
	}
}

B
Benjamin Pasero 已提交
244
export function matchesCamelCase(word: string, camelCaseWord: string): IMatch[] {
245
	if (!camelCaseWord || camelCaseWord.length === 0) {
E
Erich Gamma 已提交
246 247 248 249 250 251 252
		return null;
	}

	if (!isCamelCasePattern(word)) {
		return null;
	}

J
Joao Moreno 已提交
253
	if (camelCaseWord.length > 60) {
E
Erich Gamma 已提交
254 255 256
		return null;
	}

J
Joao Moreno 已提交
257 258 259 260 261 262 263 264 265 266
	const analysis = analyzeCamelCaseWord(camelCaseWord);

	if (!isCamelCaseWord(analysis)) {
		if (!isUpperCaseWord(analysis)) {
			return null;
		}

		camelCaseWord = camelCaseWord.toLowerCase();
	}

B
Benjamin Pasero 已提交
267 268
	let result: IMatch[] = null;
	let i = 0;
E
Erich Gamma 已提交
269 270 271 272 273 274 275 276

	while (i < camelCaseWord.length && (result = _matchesCamelCase(word.toLowerCase(), camelCaseWord, 0, i)) === null) {
		i = nextAnchor(camelCaseWord, i + 1);
	}

	return result;
}

277
// Matches beginning of words supporting non-ASCII languages
S
#17646:  
Sandeep Somavarapu 已提交
278 279
// If `contiguous` is true then matches word with beginnings of the words in the target. E.g. "pul" will match "Git: Pull"
// Otherwise also matches sub string of the word with beginnings of the words in the target. E.g. "gp" or "g p" will match "Git: Pull"
280 281
// Useful in cases where the target is words (e.g. command labels)

S
#17646:  
Sandeep Somavarapu 已提交
282
export function matchesWords(word: string, target: string, contiguous: boolean = false): IMatch[] {
283
	if (!target || target.length === 0) {
284 285 286 287 288 289
		return null;
	}

	let result: IMatch[] = null;
	let i = 0;

S
#17646:  
Sandeep Somavarapu 已提交
290
	while (i < target.length && (result = _matchesWords(word.toLowerCase(), target, 0, i, contiguous)) === null) {
291 292 293 294 295 296
		i = nextWord(target, i + 1);
	}

	return result;
}

S
#17646:  
Sandeep Somavarapu 已提交
297
function _matchesWords(word: string, target: string, i: number, j: number, contiguous: boolean): IMatch[] {
298 299 300 301 302 303 304
	if (i === word.length) {
		return [];
	} else if (j === target.length) {
		return null;
	} else if (word[i] !== target[j].toLowerCase()) {
		return null;
	} else {
B
Benjamin Pasero 已提交
305
		let result: IMatch[] = null;
306
		let nextWordIndex = j + 1;
S
#17646:  
Sandeep Somavarapu 已提交
307 308 309 310 311 312
		result = _matchesWords(word, target, i + 1, j + 1, contiguous);
		if (!contiguous) {
			while (!result && (nextWordIndex = nextWord(target, nextWordIndex)) < target.length) {
				result = _matchesWords(word, target, i + 1, nextWordIndex, contiguous);
				nextWordIndex++;
			}
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
		}
		return result === null ? null : join({ start: j, end: j + 1 }, result);
	}
}

function nextWord(word: string, start: number): number {
	for (let i = start; i < word.length; i++) {
		let c = word.charCodeAt(i);
		if (isWhitespace(c) || (i > 0 && isWhitespace(word.charCodeAt(i - 1)))) {
			return i;
		}
	}
	return word.length;
}

E
Erich Gamma 已提交
328 329
// Fuzzy

B
Benjamin Pasero 已提交
330 331 332 333 334
export enum SubstringMatching {
	Contiguous,
	Separate
}

335
export const fuzzyContiguousFilter = or(matchesPrefix, matchesCamelCase, matchesContiguousSubString);
B
Benjamin Pasero 已提交
336
const fuzzySeparateFilter = or(matchesPrefix, matchesCamelCase, matchesSubString);
B
Benjamin Pasero 已提交
337
const fuzzyRegExpCache = new BoundedLinkedMap<RegExp>(10000); // bounded to 10000 elements
B
Benjamin Pasero 已提交
338

339
export function matchesFuzzy(word: string, wordToMatchAgainst: string, enableSeparateSubstringMatching = false): IMatch[] {
340 341 342
	if (typeof word !== 'string' || typeof wordToMatchAgainst !== 'string') {
		return null; // return early for invalid input
	}
E
Erich Gamma 已提交
343 344

	// Form RegExp for wildcard matches
B
Benjamin Pasero 已提交
345
	let regexp = fuzzyRegExpCache.get(word);
E
Erich Gamma 已提交
346
	if (!regexp) {
B
Benjamin Pasero 已提交
347
		regexp = new RegExp(strings.convertSimple2RegExpPattern(word), 'i');
B
Benjamin Pasero 已提交
348
		fuzzyRegExpCache.set(word, regexp);
E
Erich Gamma 已提交
349 350 351
	}

	// RegExp Filter
B
Benjamin Pasero 已提交
352
	let match: RegExpExecArray = regexp.exec(wordToMatchAgainst);
E
Erich Gamma 已提交
353
	if (match) {
B
Benjamin Pasero 已提交
354
		return [{ start: match.index, end: match.index + match[0].length }];
E
Erich Gamma 已提交
355 356 357
	}

	// Default Filter
358
	return enableSeparateSubstringMatching ? fuzzySeparateFilter(word, wordToMatchAgainst) : fuzzyContiguousFilter(word, wordToMatchAgainst);
B
Benjamin Pasero 已提交
359
}
J
Johannes Rieken 已提交
360

J
Johannes Rieken 已提交
361
export function matchesFuzzy2(pattern: string, word: string): number[] {
J
Johannes Rieken 已提交
362 363 364 365

	pattern = pattern.toLowerCase();
	word = word.toLowerCase();

J
Johannes Rieken 已提交
366
	let matches: number[] = [];
J
Johannes Rieken 已提交
367 368 369
	let patternPos = 0;
	let wordPos = 0;
	while (patternPos < pattern.length && wordPos < word.length) {
370
		if (pattern[patternPos] === word[wordPos]) {
J
Johannes Rieken 已提交
371
			patternPos += 1;
J
Johannes Rieken 已提交
372
			matches.push(wordPos);
J
Johannes Rieken 已提交
373 374 375
		}
		wordPos += 1;
	}
J
Johannes Rieken 已提交
376

J
Johannes Rieken 已提交
377 378 379 380
	if (patternPos !== pattern.length) {
		return undefined;
	}

J
Johannes Rieken 已提交
381
	return matches;
J
Johannes Rieken 已提交
382
}
J
Johannes Rieken 已提交
383

384 385
export function createMatches(position: number[]): IMatch[] {
	let ret: IMatch[] = [];
386 387 388
	if (!position) {
		return ret;
	}
389 390 391 392 393 394 395 396 397 398 399 400
	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;
}

401 402 403 404 405 406 407 408 409 410 411 412 413
function initTable() {
	const table: number[][] = [];
	const row: number[] = [0];
	for (let i = 1; i <= 100; i++) {
		row.push(-i);
	}
	for (let i = 0; i < 100; i++) {
		let thisRow = row.slice(0);
		thisRow[0] = -i;
		table.push(thisRow);
	}
	return table;
}
414

415
const _table = initTable();
J
Johannes Rieken 已提交
416
const _scores = initTable();
J
Johannes Rieken 已提交
417
const _arrows = <Arrow[][]>initTable();
418
const _debug = false;
419

420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
function printTable(table: number[][], pattern: string, patternLen: number, word: string, wordLen: number): string {
	function pad(s: string, n: number, pad = ' ') {
		while (s.length < n) {
			s = pad + s;
		}
		return s;
	}
	let ret = ` |   |${word.split('').map(c => pad(c, 3)).join('|')}\n`;

	for (let i = 0; i <= patternLen; i++) {
		if (i === 0) {
			ret += ' |';
		} else {
			ret += `${pattern[i - 1]}|`;
		}
		ret += table[i].slice(0, wordLen + 1).map(n => pad(n.toString(), 3)).join('|') + '\n';
	}
	return ret;
}

440 441
const _seps: { [ch: string]: boolean } = Object.create(null);
_seps['_'] = true;
442
_seps['-'] = true;
443 444 445 446
_seps['.'] = true;
_seps[' '] = true;
_seps['/'] = true;
_seps['\\'] = true;
447 448
_seps['\''] = true;
_seps['"'] = true;
449

J
Johannes Rieken 已提交
450 451
const enum Arrow { Top = 0b1, Diag = 0b10, Left = 0b100 }

452 453 454 455 456
export function fuzzyScore(pattern: string, word: string): [number, number[]] {

	const patternLen = pattern.length > 25 ? 25 : pattern.length;
	const wordLen = word.length > 100 ? 100 : word.length;

457 458 459 460
	if (patternLen === 0) {
		return [-1, []];
	}

461 462 463 464 465 466
	if (patternLen > wordLen) {
		return undefined;
	}

	const lowPattern = pattern.toLowerCase();
	const lowWord = word.toLowerCase();
467 468 469 470 471 472 473 474 475 476 477 478 479
	let i = 0;
	let j = 0;

	while (i < patternLen && j < wordLen) {
		if (lowPattern[i] === lowWord[j]) {
			i += 1;
		}
		j += 1;
	}
	if (i !== patternLen) {
		// no simple matches found -> return early
		return undefined;
	}
480 481 482 483 484 485 486 487 488 489

	for (i = 1; i <= patternLen; i++) {

		let lastLowWordChar = '';

		for (j = 1; j <= wordLen; j++) {

			let score = -1;
			let lowWordChar = lowWord[j - 1];
			if (lowPattern[i - 1] === lowWordChar) {
J
Johannes Rieken 已提交
490 491

				if (j === 1) {
492 493 494 495 496
					if (pattern[i - 1] === word[j - 1]) {
						score = 7;
					} else {
						score = 5;
					}
J
Johannes Rieken 已提交
497
				} else if (lowWordChar !== word[j - 1]) {
J
Johannes Rieken 已提交
498 499 500 501 502
					if (pattern[i - 1] === word[j - 1]) {
						score = 7;
					} else {
						score = 5;
					}
503
				} else if (_seps[lastLowWordChar]) {
J
Johannes Rieken 已提交
504 505
					score = 5;

J
Johannes Rieken 已提交
506 507
				} else if (j === i) {
					score = 3;
508 509 510 511

				} else {
					score = 1;
				}
J
Johannes Rieken 已提交
512 513 514 515

				if (i > 1 && j > 1 && _scores[i - 1][j - 1] > score) {
					score = _scores[i - 1][j - 1];
				}
516 517
			}

J
Johannes Rieken 已提交
518 519
			_scores[i][j] = score;

520 521 522 523 524 525
			let diag = _table[i - 1][j - 1] + score;
			let top = _table[i - 1][j] + -1;
			let left = _table[i][j - 1] + -1;

			if (left >= top) {
				// left or diag
J
Johannes Rieken 已提交
526
				if (left > diag) {
527
					_table[i][j] = left;
J
Johannes Rieken 已提交
528
					_arrows[i][j] = Arrow.Left;
J
Johannes Rieken 已提交
529 530 531
				} else if (left === diag) {
					_table[i][j] = left;
					_arrows[i][j] = Arrow.Left | Arrow.Diag;
532 533
				} else {
					_table[i][j] = diag;
J
Johannes Rieken 已提交
534
					_arrows[i][j] = Arrow.Diag;
535 536 537
				}
			} else {
				// top or diag
J
Johannes Rieken 已提交
538
				if (top > diag) {
539
					_table[i][j] = top;
J
Johannes Rieken 已提交
540
					_arrows[i][j] = Arrow.Top;
J
Johannes Rieken 已提交
541 542 543 544 545 546
				} else if (top === diag) {
					_table[i][j] = top;
					_arrows[i][j] = Arrow.Top | Arrow.Diag;
				} else {
					_table[i][j] = diag;
					_arrows[i][j] = Arrow.Diag;
547 548 549 550 551 552 553 554 555 556
				}
			}

			lastLowWordChar = lowWordChar;
		}
	}

	if (_debug) {
		console.log(printTable(_table, pattern, patternLen, word, wordLen));
		console.log(printTable(_arrows, pattern, patternLen, word, wordLen));
J
Johannes Rieken 已提交
557
		console.log(printTable(_scores, pattern, patternLen, word, wordLen));
558 559
	}

J
Johannes Rieken 已提交
560
	let bucket: [number, number[]][] = [];
561
	findAllMatches(patternLen, patternLen, wordLen, 0, [], bucket, false);
J
Johannes Rieken 已提交
562

J
Johannes Rieken 已提交
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578
	if (bucket.length === 0) {
		return undefined;
	}

	let topMatch = bucket.shift();
	for (const match of bucket) {
		if (!topMatch || topMatch[0] < match[0]) {
			topMatch = match;
		}
	}
	if (_debug) {
		console.log(`${pattern} & ${word} => ${topMatch[0]} points for ${topMatch[1]}`);
	}
	return topMatch;
}

579
function findAllMatches(patternLen: number, patternPos: number, wordPos: number, total: number, matches: number[], bucket: [number, number[]][], lastMatched: boolean): void {
580

J
Johannes Rieken 已提交
581
	while (patternPos > 0 && wordPos > 0) {
582

J
Johannes Rieken 已提交
583 584 585 586 587 588
		let score = _scores[patternPos][wordPos];
		let arrow = _arrows[patternPos][wordPos];

		if (arrow === Arrow.Left || score < 0) {
			// left
			wordPos -= 1;
589 590 591 592
			if (lastMatched) {
				total -= 5; // gap penalty
			}
			lastMatched = false;
J
Johannes Rieken 已提交
593 594 595 596 597

		} else if (arrow & Arrow.Diag) {

			if (arrow & Arrow.Left) {
				// left
598
				findAllMatches(patternLen, patternPos, wordPos - 1, total, matches.slice(0), bucket, lastMatched);
599
			}
J
Johannes Rieken 已提交
600 601

			// diag
602
			total += score;
J
Johannes Rieken 已提交
603 604 605
			patternPos -= 1;
			wordPos -= 1;
			matches.unshift(wordPos);
606
			lastMatched = true;
J
Johannes Rieken 已提交
607

J
Johannes Rieken 已提交
608
		} else {
609
			return undefined;
610 611 612
		}
	}

J
Johannes Rieken 已提交
613 614 615
	if (matches.length !== patternLen) {
		// doesn't cover whole pattern
		return undefined;
616
	}
617

J
Johannes Rieken 已提交
618 619 620
	if (_scores[1][matches[0] + 1] === 1) {
		// first match is weak
		return undefined;
621 622
	}

J
Johannes Rieken 已提交
623 624 625 626
	total -= wordPos >= 3 ? 9 : wordPos * 3; // late start penalty
	total -= (1 + matches[matches.length - 1]) - patternLen; // penalty for all non matching characters between first and last

	bucket.push([total, matches]);
627
}
628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649


export function nextTypoPermutation(pattern: string, patternPos: number) {

	if (patternPos + 1 >= pattern.length) {
		return undefined;
	}

	return pattern.slice(0, patternPos)
		+ pattern[patternPos + 1]
		+ pattern[patternPos]
		+ pattern.slice(patternPos + 2);
}

export function fuzzyScoreGraceful(pattern: string, word: string): [number, number[]] {
	let ret = fuzzyScore(pattern, word);
	for (let patternPos = 1; patternPos < pattern.length - 1 && !ret; patternPos++) {
		let pattern2 = nextTypoPermutation(pattern, patternPos);
		ret = fuzzyScore(pattern2, word);
	}
	return ret;
}