8348c00e39f814021e8c5fed76ad7522750d77e0aa0e9bfb90255ac23ade754e3f3300d2711569326db2bf5055ce2b5dc91fd1203325c3c48bb054da86a7fb 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { LRUCache } from './map.js';
  6. import * as strings from './strings.js';
  7. // Combined filters
  8. /**
  9. * @returns A filter which combines the provided set
  10. * of filters with an or. The *first* filters that
  11. * matches defined the return value of the returned
  12. * filter.
  13. */
  14. export function or(...filter) {
  15. return function (word, wordToMatchAgainst) {
  16. for (let i = 0, len = filter.length; i < len; i++) {
  17. const match = filter[i](word, wordToMatchAgainst);
  18. if (match) {
  19. return match;
  20. }
  21. }
  22. return null;
  23. };
  24. }
  25. // Prefix
  26. export const matchesStrictPrefix = _matchesPrefix.bind(undefined, false);
  27. export const matchesPrefix = _matchesPrefix.bind(undefined, true);
  28. function _matchesPrefix(ignoreCase, word, wordToMatchAgainst) {
  29. if (!wordToMatchAgainst || wordToMatchAgainst.length < word.length) {
  30. return null;
  31. }
  32. let matches;
  33. if (ignoreCase) {
  34. matches = strings.startsWithIgnoreCase(wordToMatchAgainst, word);
  35. }
  36. else {
  37. matches = wordToMatchAgainst.indexOf(word) === 0;
  38. }
  39. if (!matches) {
  40. return null;
  41. }
  42. return word.length > 0 ? [{ start: 0, end: word.length }] : [];
  43. }
  44. // Contiguous Substring
  45. export function matchesContiguousSubString(word, wordToMatchAgainst) {
  46. const index = wordToMatchAgainst.toLowerCase().indexOf(word.toLowerCase());
  47. if (index === -1) {
  48. return null;
  49. }
  50. return [{ start: index, end: index + word.length }];
  51. }
  52. // Substring
  53. export function matchesSubString(word, wordToMatchAgainst) {
  54. return _matchesSubString(word.toLowerCase(), wordToMatchAgainst.toLowerCase(), 0, 0);
  55. }
  56. function _matchesSubString(word, wordToMatchAgainst, i, j) {
  57. if (i === word.length) {
  58. return [];
  59. }
  60. else if (j === wordToMatchAgainst.length) {
  61. return null;
  62. }
  63. else {
  64. if (word[i] === wordToMatchAgainst[j]) {
  65. let result = null;
  66. if (result = _matchesSubString(word, wordToMatchAgainst, i + 1, j + 1)) {
  67. return join({ start: j, end: j + 1 }, result);
  68. }
  69. return null;
  70. }
  71. return _matchesSubString(word, wordToMatchAgainst, i, j + 1);
  72. }
  73. }
  74. // CamelCase
  75. function isLower(code) {
  76. return 97 /* CharCode.a */ <= code && code <= 122 /* CharCode.z */;
  77. }
  78. export function isUpper(code) {
  79. return 65 /* CharCode.A */ <= code && code <= 90 /* CharCode.Z */;
  80. }
  81. function isNumber(code) {
  82. return 48 /* CharCode.Digit0 */ <= code && code <= 57 /* CharCode.Digit9 */;
  83. }
  84. function isWhitespace(code) {
  85. return (code === 32 /* CharCode.Space */
  86. || code === 9 /* CharCode.Tab */
  87. || code === 10 /* CharCode.LineFeed */
  88. || code === 13 /* CharCode.CarriageReturn */);
  89. }
  90. const wordSeparators = new Set();
  91. // These are chosen as natural word separators based on writen text.
  92. // It is a subset of the word separators used by the monaco editor.
  93. '()[]{}<>`\'"-/;:,.?!'
  94. .split('')
  95. .forEach(s => wordSeparators.add(s.charCodeAt(0)));
  96. function isWordSeparator(code) {
  97. return isWhitespace(code) || wordSeparators.has(code);
  98. }
  99. function charactersMatch(codeA, codeB) {
  100. return (codeA === codeB) || (isWordSeparator(codeA) && isWordSeparator(codeB));
  101. }
  102. function isAlphanumeric(code) {
  103. return isLower(code) || isUpper(code) || isNumber(code);
  104. }
  105. function join(head, tail) {
  106. if (tail.length === 0) {
  107. tail = [head];
  108. }
  109. else if (head.end === tail[0].start) {
  110. tail[0].start = head.start;
  111. }
  112. else {
  113. tail.unshift(head);
  114. }
  115. return tail;
  116. }
  117. function nextAnchor(camelCaseWord, start) {
  118. for (let i = start; i < camelCaseWord.length; i++) {
  119. const c = camelCaseWord.charCodeAt(i);
  120. if (isUpper(c) || isNumber(c) || (i > 0 && !isAlphanumeric(camelCaseWord.charCodeAt(i - 1)))) {
  121. return i;
  122. }
  123. }
  124. return camelCaseWord.length;
  125. }
  126. function _matchesCamelCase(word, camelCaseWord, i, j) {
  127. if (i === word.length) {
  128. return [];
  129. }
  130. else if (j === camelCaseWord.length) {
  131. return null;
  132. }
  133. else if (word[i] !== camelCaseWord[j].toLowerCase()) {
  134. return null;
  135. }
  136. else {
  137. let result = null;
  138. let nextUpperIndex = j + 1;
  139. result = _matchesCamelCase(word, camelCaseWord, i + 1, j + 1);
  140. while (!result && (nextUpperIndex = nextAnchor(camelCaseWord, nextUpperIndex)) < camelCaseWord.length) {
  141. result = _matchesCamelCase(word, camelCaseWord, i + 1, nextUpperIndex);
  142. nextUpperIndex++;
  143. }
  144. return result === null ? null : join({ start: j, end: j + 1 }, result);
  145. }
  146. }
  147. // Heuristic to avoid computing camel case matcher for words that don't
  148. // look like camelCaseWords.
  149. function analyzeCamelCaseWord(word) {
  150. let upper = 0, lower = 0, alpha = 0, numeric = 0, code = 0;
  151. for (let i = 0; i < word.length; i++) {
  152. code = word.charCodeAt(i);
  153. if (isUpper(code)) {
  154. upper++;
  155. }
  156. if (isLower(code)) {
  157. lower++;
  158. }
  159. if (isAlphanumeric(code)) {
  160. alpha++;
  161. }
  162. if (isNumber(code)) {
  163. numeric++;
  164. }
  165. }
  166. const upperPercent = upper / word.length;
  167. const lowerPercent = lower / word.length;
  168. const alphaPercent = alpha / word.length;
  169. const numericPercent = numeric / word.length;
  170. return { upperPercent, lowerPercent, alphaPercent, numericPercent };
  171. }
  172. function isUpperCaseWord(analysis) {
  173. const { upperPercent, lowerPercent } = analysis;
  174. return lowerPercent === 0 && upperPercent > 0.6;
  175. }
  176. function isCamelCaseWord(analysis) {
  177. const { upperPercent, lowerPercent, alphaPercent, numericPercent } = analysis;
  178. return lowerPercent > 0.2 && upperPercent < 0.8 && alphaPercent > 0.6 && numericPercent < 0.2;
  179. }
  180. // Heuristic to avoid computing camel case matcher for words that don't
  181. // look like camel case patterns.
  182. function isCamelCasePattern(word) {
  183. let upper = 0, lower = 0, code = 0, whitespace = 0;
  184. for (let i = 0; i < word.length; i++) {
  185. code = word.charCodeAt(i);
  186. if (isUpper(code)) {
  187. upper++;
  188. }
  189. if (isLower(code)) {
  190. lower++;
  191. }
  192. if (isWhitespace(code)) {
  193. whitespace++;
  194. }
  195. }
  196. if ((upper === 0 || lower === 0) && whitespace === 0) {
  197. return word.length <= 30;
  198. }
  199. else {
  200. return upper <= 5;
  201. }
  202. }
  203. export function matchesCamelCase(word, camelCaseWord) {
  204. if (!camelCaseWord) {
  205. return null;
  206. }
  207. camelCaseWord = camelCaseWord.trim();
  208. if (camelCaseWord.length === 0) {
  209. return null;
  210. }
  211. if (!isCamelCasePattern(word)) {
  212. return null;
  213. }
  214. if (camelCaseWord.length > 60) {
  215. return null;
  216. }
  217. const analysis = analyzeCamelCaseWord(camelCaseWord);
  218. if (!isCamelCaseWord(analysis)) {
  219. if (!isUpperCaseWord(analysis)) {
  220. return null;
  221. }
  222. camelCaseWord = camelCaseWord.toLowerCase();
  223. }
  224. let result = null;
  225. let i = 0;
  226. word = word.toLowerCase();
  227. while (i < camelCaseWord.length && (result = _matchesCamelCase(word, camelCaseWord, 0, i)) === null) {
  228. i = nextAnchor(camelCaseWord, i + 1);
  229. }
  230. return result;
  231. }
  232. // Matches beginning of words supporting non-ASCII languages
  233. // If `contiguous` is true then matches word with beginnings of the words in the target. E.g. "pul" will match "Git: Pull"
  234. // 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"
  235. // Useful in cases where the target is words (e.g. command labels)
  236. export function matchesWords(word, target, contiguous = false) {
  237. if (!target || target.length === 0) {
  238. return null;
  239. }
  240. let result = null;
  241. let i = 0;
  242. word = word.toLowerCase();
  243. target = target.toLowerCase();
  244. while (i < target.length && (result = _matchesWords(word, target, 0, i, contiguous)) === null) {
  245. i = nextWord(target, i + 1);
  246. }
  247. return result;
  248. }
  249. function _matchesWords(word, target, i, j, contiguous) {
  250. if (i === word.length) {
  251. return [];
  252. }
  253. else if (j === target.length) {
  254. return null;
  255. }
  256. else if (!charactersMatch(word.charCodeAt(i), target.charCodeAt(j))) {
  257. return null;
  258. }
  259. else {
  260. let result = null;
  261. let nextWordIndex = j + 1;
  262. result = _matchesWords(word, target, i + 1, j + 1, contiguous);
  263. if (!contiguous) {
  264. while (!result && (nextWordIndex = nextWord(target, nextWordIndex)) < target.length) {
  265. result = _matchesWords(word, target, i + 1, nextWordIndex, contiguous);
  266. nextWordIndex++;
  267. }
  268. }
  269. return result === null ? null : join({ start: j, end: j + 1 }, result);
  270. }
  271. }
  272. function nextWord(word, start) {
  273. for (let i = start; i < word.length; i++) {
  274. if (isWordSeparator(word.charCodeAt(i)) ||
  275. (i > 0 && isWordSeparator(word.charCodeAt(i - 1)))) {
  276. return i;
  277. }
  278. }
  279. return word.length;
  280. }
  281. // Fuzzy
  282. const fuzzyContiguousFilter = or(matchesPrefix, matchesCamelCase, matchesContiguousSubString);
  283. const fuzzySeparateFilter = or(matchesPrefix, matchesCamelCase, matchesSubString);
  284. const fuzzyRegExpCache = new LRUCache(10000); // bounded to 10000 elements
  285. export function matchesFuzzy(word, wordToMatchAgainst, enableSeparateSubstringMatching = false) {
  286. if (typeof word !== 'string' || typeof wordToMatchAgainst !== 'string') {
  287. return null; // return early for invalid input
  288. }
  289. // Form RegExp for wildcard matches
  290. let regexp = fuzzyRegExpCache.get(word);
  291. if (!regexp) {
  292. regexp = new RegExp(strings.convertSimple2RegExpPattern(word), 'i');
  293. fuzzyRegExpCache.set(word, regexp);
  294. }
  295. // RegExp Filter
  296. const match = regexp.exec(wordToMatchAgainst);
  297. if (match) {
  298. return [{ start: match.index, end: match.index + match[0].length }];
  299. }
  300. // Default Filter
  301. return enableSeparateSubstringMatching ? fuzzySeparateFilter(word, wordToMatchAgainst) : fuzzyContiguousFilter(word, wordToMatchAgainst);
  302. }
  303. export function anyScore(pattern, lowPattern, patternPos, word, lowWord, wordPos) {
  304. const max = Math.min(13, pattern.length);
  305. for (; patternPos < max; patternPos++) {
  306. const result = fuzzyScore(pattern, lowPattern, patternPos, word, lowWord, wordPos, { firstMatchCanBeWeak: false, boostFullMatch: true });
  307. if (result) {
  308. return result;
  309. }
  310. }
  311. return [0, wordPos];
  312. }
  313. //#region --- fuzzyScore ---
  314. export function createMatches(score) {
  315. if (typeof score === 'undefined') {
  316. return [];
  317. }
  318. const res = [];
  319. const wordPos = score[1];
  320. for (let i = score.length - 1; i > 1; i--) {
  321. const pos = score[i] + wordPos;
  322. const last = res[res.length - 1];
  323. if (last && last.end === pos) {
  324. last.end = pos + 1;
  325. }
  326. else {
  327. res.push({ start: pos, end: pos + 1 });
  328. }
  329. }
  330. return res;
  331. }
  332. const _maxLen = 128;
  333. function initTable() {
  334. const table = [];
  335. const row = [];
  336. for (let i = 0; i <= _maxLen; i++) {
  337. row[i] = 0;
  338. }
  339. for (let i = 0; i <= _maxLen; i++) {
  340. table.push(row.slice(0));
  341. }
  342. return table;
  343. }
  344. function initArr(maxLen) {
  345. const row = [];
  346. for (let i = 0; i <= maxLen; i++) {
  347. row[i] = 0;
  348. }
  349. return row;
  350. }
  351. const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
  352. const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
  353. const _diag = initTable(); // the length of a contiguous diagonal match
  354. const _table = initTable();
  355. const _arrows = initTable();
  356. const _debug = false;
  357. function printTable(table, pattern, patternLen, word, wordLen) {
  358. function pad(s, n, pad = ' ') {
  359. while (s.length < n) {
  360. s = pad + s;
  361. }
  362. return s;
  363. }
  364. let ret = ` | |${word.split('').map(c => pad(c, 3)).join('|')}\n`;
  365. for (let i = 0; i <= patternLen; i++) {
  366. if (i === 0) {
  367. ret += ' |';
  368. }
  369. else {
  370. ret += `${pattern[i - 1]}|`;
  371. }
  372. ret += table[i].slice(0, wordLen + 1).map(n => pad(n.toString(), 3)).join('|') + '\n';
  373. }
  374. return ret;
  375. }
  376. function printTables(pattern, patternStart, word, wordStart) {
  377. pattern = pattern.substr(patternStart);
  378. word = word.substr(wordStart);
  379. console.log(printTable(_table, pattern, pattern.length, word, word.length));
  380. console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
  381. console.log(printTable(_diag, pattern, pattern.length, word, word.length));
  382. }
  383. function isSeparatorAtPos(value, index) {
  384. if (index < 0 || index >= value.length) {
  385. return false;
  386. }
  387. const code = value.codePointAt(index);
  388. switch (code) {
  389. case 95 /* CharCode.Underline */:
  390. case 45 /* CharCode.Dash */:
  391. case 46 /* CharCode.Period */:
  392. case 32 /* CharCode.Space */:
  393. case 47 /* CharCode.Slash */:
  394. case 92 /* CharCode.Backslash */:
  395. case 39 /* CharCode.SingleQuote */:
  396. case 34 /* CharCode.DoubleQuote */:
  397. case 58 /* CharCode.Colon */:
  398. case 36 /* CharCode.DollarSign */:
  399. case 60 /* CharCode.LessThan */:
  400. case 62 /* CharCode.GreaterThan */:
  401. case 40 /* CharCode.OpenParen */:
  402. case 41 /* CharCode.CloseParen */:
  403. case 91 /* CharCode.OpenSquareBracket */:
  404. case 93 /* CharCode.CloseSquareBracket */:
  405. case 123 /* CharCode.OpenCurlyBrace */:
  406. case 125 /* CharCode.CloseCurlyBrace */:
  407. return true;
  408. case undefined:
  409. return false;
  410. default:
  411. if (strings.isEmojiImprecise(code)) {
  412. return true;
  413. }
  414. return false;
  415. }
  416. }
  417. function isWhitespaceAtPos(value, index) {
  418. if (index < 0 || index >= value.length) {
  419. return false;
  420. }
  421. const code = value.charCodeAt(index);
  422. switch (code) {
  423. case 32 /* CharCode.Space */:
  424. case 9 /* CharCode.Tab */:
  425. return true;
  426. default:
  427. return false;
  428. }
  429. }
  430. function isUpperCaseAtPos(pos, word, wordLow) {
  431. return word[pos] !== wordLow[pos];
  432. }
  433. export function isPatternInWord(patternLow, patternPos, patternLen, wordLow, wordPos, wordLen, fillMinWordPosArr = false) {
  434. while (patternPos < patternLen && wordPos < wordLen) {
  435. if (patternLow[patternPos] === wordLow[wordPos]) {
  436. if (fillMinWordPosArr) {
  437. // Remember the min word position for each pattern position
  438. _minWordMatchPos[patternPos] = wordPos;
  439. }
  440. patternPos += 1;
  441. }
  442. wordPos += 1;
  443. }
  444. return patternPos === patternLen; // pattern must be exhausted
  445. }
  446. export var FuzzyScore;
  447. (function (FuzzyScore) {
  448. /**
  449. * No matches and value `-100`
  450. */
  451. FuzzyScore.Default = ([-100, 0]);
  452. function isDefault(score) {
  453. return !score || (score.length === 2 && score[0] === -100 && score[1] === 0);
  454. }
  455. FuzzyScore.isDefault = isDefault;
  456. })(FuzzyScore || (FuzzyScore = {}));
  457. export class FuzzyScoreOptions {
  458. constructor(firstMatchCanBeWeak, boostFullMatch) {
  459. this.firstMatchCanBeWeak = firstMatchCanBeWeak;
  460. this.boostFullMatch = boostFullMatch;
  461. }
  462. }
  463. FuzzyScoreOptions.default = { boostFullMatch: true, firstMatchCanBeWeak: false };
  464. export function fuzzyScore(pattern, patternLow, patternStart, word, wordLow, wordStart, options = FuzzyScoreOptions.default) {
  465. const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
  466. const wordLen = word.length > _maxLen ? _maxLen : word.length;
  467. if (patternStart >= patternLen || wordStart >= wordLen || (patternLen - patternStart) > (wordLen - wordStart)) {
  468. return undefined;
  469. }
  470. // Run a simple check if the characters of pattern occur
  471. // (in order) at all in word. If that isn't the case we
  472. // stop because no match will be possible
  473. if (!isPatternInWord(patternLow, patternStart, patternLen, wordLow, wordStart, wordLen, true)) {
  474. return undefined;
  475. }
  476. // Find the max matching word position for each pattern position
  477. // NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
  478. _fillInMaxWordMatchPos(patternLen, wordLen, patternStart, wordStart, patternLow, wordLow);
  479. let row = 1;
  480. let column = 1;
  481. let patternPos = patternStart;
  482. let wordPos = wordStart;
  483. const hasStrongFirstMatch = [false];
  484. // There will be a match, fill in tables
  485. for (row = 1, patternPos = patternStart; patternPos < patternLen; row++, patternPos++) {
  486. // Reduce search space to possible matching word positions and to possible access from next row
  487. const minWordMatchPos = _minWordMatchPos[patternPos];
  488. const maxWordMatchPos = _maxWordMatchPos[patternPos];
  489. const nextMaxWordMatchPos = (patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen);
  490. for (column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos; wordPos < nextMaxWordMatchPos; column++, wordPos++) {
  491. let score = Number.MIN_SAFE_INTEGER;
  492. let canComeDiag = false;
  493. if (wordPos <= maxWordMatchPos) {
  494. score = _doScore(pattern, patternLow, patternPos, patternStart, word, wordLow, wordPos, wordLen, wordStart, _diag[row - 1][column - 1] === 0, hasStrongFirstMatch);
  495. }
  496. let diagScore = 0;
  497. if (score !== Number.MAX_SAFE_INTEGER) {
  498. canComeDiag = true;
  499. diagScore = score + _table[row - 1][column - 1];
  500. }
  501. const canComeLeft = wordPos > minWordMatchPos;
  502. const leftScore = canComeLeft ? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0) : 0; // penalty for a gap start
  503. const canComeLeftLeft = wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
  504. const leftLeftScore = canComeLeftLeft ? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0) : 0; // penalty for a gap start
  505. if (canComeLeftLeft && (!canComeLeft || leftLeftScore >= leftScore) && (!canComeDiag || leftLeftScore >= diagScore)) {
  506. // always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
  507. _table[row][column] = leftLeftScore;
  508. _arrows[row][column] = 3 /* Arrow.LeftLeft */;
  509. _diag[row][column] = 0;
  510. }
  511. else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
  512. // always prefer choosing left since that means a match is earlier in the word
  513. _table[row][column] = leftScore;
  514. _arrows[row][column] = 2 /* Arrow.Left */;
  515. _diag[row][column] = 0;
  516. }
  517. else if (canComeDiag) {
  518. _table[row][column] = diagScore;
  519. _arrows[row][column] = 1 /* Arrow.Diag */;
  520. _diag[row][column] = _diag[row - 1][column - 1] + 1;
  521. }
  522. else {
  523. throw new Error(`not possible`);
  524. }
  525. }
  526. }
  527. if (_debug) {
  528. printTables(pattern, patternStart, word, wordStart);
  529. }
  530. if (!hasStrongFirstMatch[0] && !options.firstMatchCanBeWeak) {
  531. return undefined;
  532. }
  533. row--;
  534. column--;
  535. const result = [_table[row][column], wordStart];
  536. let backwardsDiagLength = 0;
  537. let maxMatchColumn = 0;
  538. while (row >= 1) {
  539. // Find the column where we go diagonally up
  540. let diagColumn = column;
  541. do {
  542. const arrow = _arrows[row][diagColumn];
  543. if (arrow === 3 /* Arrow.LeftLeft */) {
  544. diagColumn = diagColumn - 2;
  545. }
  546. else if (arrow === 2 /* Arrow.Left */) {
  547. diagColumn = diagColumn - 1;
  548. }
  549. else {
  550. // found the diagonal
  551. break;
  552. }
  553. } while (diagColumn >= 1);
  554. // Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
  555. if (backwardsDiagLength > 1 // only if we would have a contiguous match of 3 characters
  556. && patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] // only if we can do a contiguous match diagonally
  557. && !isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) // only if the forwards chose diagonal is not an uppercase
  558. && backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
  559. ) {
  560. diagColumn = column;
  561. }
  562. if (diagColumn === column) {
  563. // this is a contiguous match
  564. backwardsDiagLength++;
  565. }
  566. else {
  567. backwardsDiagLength = 1;
  568. }
  569. if (!maxMatchColumn) {
  570. // remember the last matched column
  571. maxMatchColumn = diagColumn;
  572. }
  573. row--;
  574. column = diagColumn - 1;
  575. result.push(column);
  576. }
  577. if (wordLen === patternLen && options.boostFullMatch) {
  578. // the word matches the pattern with all characters!
  579. // giving the score a total match boost (to come up ahead other words)
  580. result[0] += 2;
  581. }
  582. // Add 1 penalty for each skipped character in the word
  583. const skippedCharsCount = maxMatchColumn - patternLen;
  584. result[0] -= skippedCharsCount;
  585. return result;
  586. }
  587. function _fillInMaxWordMatchPos(patternLen, wordLen, patternStart, wordStart, patternLow, wordLow) {
  588. let patternPos = patternLen - 1;
  589. let wordPos = wordLen - 1;
  590. while (patternPos >= patternStart && wordPos >= wordStart) {
  591. if (patternLow[patternPos] === wordLow[wordPos]) {
  592. _maxWordMatchPos[patternPos] = wordPos;
  593. patternPos--;
  594. }
  595. wordPos--;
  596. }
  597. }
  598. function _doScore(pattern, patternLow, patternPos, patternStart, word, wordLow, wordPos, wordLen, wordStart, newMatchStart, outFirstMatchStrong) {
  599. if (patternLow[patternPos] !== wordLow[wordPos]) {
  600. return Number.MIN_SAFE_INTEGER;
  601. }
  602. let score = 1;
  603. let isGapLocation = false;
  604. if (wordPos === (patternPos - patternStart)) {
  605. // common prefix: `foobar <-> foobaz`
  606. // ^^^^^
  607. score = pattern[patternPos] === word[wordPos] ? 7 : 5;
  608. }
  609. else if (isUpperCaseAtPos(wordPos, word, wordLow) && (wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))) {
  610. // hitting upper-case: `foo <-> forOthers`
  611. // ^^ ^
  612. score = pattern[patternPos] === word[wordPos] ? 7 : 5;
  613. isGapLocation = true;
  614. }
  615. else if (isSeparatorAtPos(wordLow, wordPos) && (wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))) {
  616. // hitting a separator: `. <-> foo.bar`
  617. // ^
  618. score = 5;
  619. }
  620. else if (isSeparatorAtPos(wordLow, wordPos - 1) || isWhitespaceAtPos(wordLow, wordPos - 1)) {
  621. // post separator: `foo <-> bar_foo`
  622. // ^^^
  623. score = 5;
  624. isGapLocation = true;
  625. }
  626. if (score > 1 && patternPos === patternStart) {
  627. outFirstMatchStrong[0] = true;
  628. }
  629. if (!isGapLocation) {
  630. isGapLocation = isUpperCaseAtPos(wordPos, word, wordLow) || isSeparatorAtPos(wordLow, wordPos - 1) || isWhitespaceAtPos(wordLow, wordPos - 1);
  631. }
  632. //
  633. if (patternPos === patternStart) { // first character in pattern
  634. if (wordPos > wordStart) {
  635. // the first pattern character would match a word character that is not at the word start
  636. // so introduce a penalty to account for the gap preceding this match
  637. score -= isGapLocation ? 3 : 5;
  638. }
  639. }
  640. else {
  641. if (newMatchStart) {
  642. // this would be the beginning of a new match (i.e. there would be a gap before this location)
  643. score += isGapLocation ? 2 : 0;
  644. }
  645. else {
  646. // this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a preferred gap location
  647. score += isGapLocation ? 0 : 1;
  648. }
  649. }
  650. if (wordPos + 1 === wordLen) {
  651. // we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
  652. // so pretend there is a gap after the last character in the word to normalize things
  653. score -= isGapLocation ? 3 : 5;
  654. }
  655. return score;
  656. }
  657. //#endregion
  658. //#region --- graceful ---
  659. export function fuzzyScoreGracefulAggressive(pattern, lowPattern, patternPos, word, lowWord, wordPos, options) {
  660. return fuzzyScoreWithPermutations(pattern, lowPattern, patternPos, word, lowWord, wordPos, true, options);
  661. }
  662. function fuzzyScoreWithPermutations(pattern, lowPattern, patternPos, word, lowWord, wordPos, aggressive, options) {
  663. let top = fuzzyScore(pattern, lowPattern, patternPos, word, lowWord, wordPos, options);
  664. if (top && !aggressive) {
  665. // when using the original pattern yield a result we`
  666. // return it unless we are aggressive and try to find
  667. // a better alignment, e.g. `cno` -> `^co^ns^ole` or `^c^o^nsole`.
  668. return top;
  669. }
  670. if (pattern.length >= 3) {
  671. // When the pattern is long enough then try a few (max 7)
  672. // permutations of the pattern to find a better match. The
  673. // permutations only swap neighbouring characters, e.g
  674. // `cnoso` becomes `conso`, `cnsoo`, `cnoos`.
  675. const tries = Math.min(7, pattern.length - 1);
  676. for (let movingPatternPos = patternPos + 1; movingPatternPos < tries; movingPatternPos++) {
  677. const newPattern = nextTypoPermutation(pattern, movingPatternPos);
  678. if (newPattern) {
  679. const candidate = fuzzyScore(newPattern, newPattern.toLowerCase(), patternPos, word, lowWord, wordPos, options);
  680. if (candidate) {
  681. candidate[0] -= 3; // permutation penalty
  682. if (!top || candidate[0] > top[0]) {
  683. top = candidate;
  684. }
  685. }
  686. }
  687. }
  688. }
  689. return top;
  690. }
  691. function nextTypoPermutation(pattern, patternPos) {
  692. if (patternPos + 1 >= pattern.length) {
  693. return undefined;
  694. }
  695. const swap1 = pattern[patternPos];
  696. const swap2 = pattern[patternPos + 1];
  697. if (swap1 === swap2) {
  698. return undefined;
  699. }
  700. return pattern.slice(0, patternPos)
  701. + swap2
  702. + swap1
  703. + pattern.slice(patternPos + 2);
  704. }
  705. //#endregion