399300a5ddf21b28a311613765b986cf3d1070a7be4145e3591eb2988ab1c607eca78ccdebe3fba92ef2939c34bae6bd63328992b41bca069414293d1acd13 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { createMatches as createFuzzyMatches, fuzzyScore } from './filters.js';
  2. import { sep } from './path.js';
  3. import { isWindows } from './platform.js';
  4. import { stripWildcards } from './strings.js';
  5. const NO_SCORE2 = [undefined, []];
  6. export function scoreFuzzy2(target, query, patternStart = 0, wordStart = 0) {
  7. // Score: multiple inputs
  8. const preparedQuery = query;
  9. if (preparedQuery.values && preparedQuery.values.length > 1) {
  10. return doScoreFuzzy2Multiple(target, preparedQuery.values, patternStart, wordStart);
  11. }
  12. // Score: single input
  13. return doScoreFuzzy2Single(target, query, patternStart, wordStart);
  14. }
  15. function doScoreFuzzy2Multiple(target, query, patternStart, wordStart) {
  16. let totalScore = 0;
  17. const totalMatches = [];
  18. for (const queryPiece of query) {
  19. const [score, matches] = doScoreFuzzy2Single(target, queryPiece, patternStart, wordStart);
  20. if (typeof score !== 'number') {
  21. // if a single query value does not match, return with
  22. // no score entirely, we require all queries to match
  23. return NO_SCORE2;
  24. }
  25. totalScore += score;
  26. totalMatches.push(...matches);
  27. }
  28. // if we have a score, ensure that the positions are
  29. // sorted in ascending order and distinct
  30. return [totalScore, normalizeMatches(totalMatches)];
  31. }
  32. function doScoreFuzzy2Single(target, query, patternStart, wordStart) {
  33. const score = fuzzyScore(query.original, query.originalLowercase, patternStart, target, target.toLowerCase(), wordStart, { firstMatchCanBeWeak: true, boostFullMatch: true });
  34. if (!score) {
  35. return NO_SCORE2;
  36. }
  37. return [score[0], createFuzzyMatches(score)];
  38. }
  39. const NO_ITEM_SCORE = Object.freeze({ score: 0 });
  40. function normalizeMatches(matches) {
  41. // sort matches by start to be able to normalize
  42. const sortedMatches = matches.sort((matchA, matchB) => {
  43. return matchA.start - matchB.start;
  44. });
  45. // merge matches that overlap
  46. const normalizedMatches = [];
  47. let currentMatch = undefined;
  48. for (const match of sortedMatches) {
  49. // if we have no current match or the matches
  50. // do not overlap, we take it as is and remember
  51. // it for future merging
  52. if (!currentMatch || !matchOverlaps(currentMatch, match)) {
  53. currentMatch = match;
  54. normalizedMatches.push(match);
  55. }
  56. // otherwise we merge the matches
  57. else {
  58. currentMatch.start = Math.min(currentMatch.start, match.start);
  59. currentMatch.end = Math.max(currentMatch.end, match.end);
  60. }
  61. }
  62. return normalizedMatches;
  63. }
  64. function matchOverlaps(matchA, matchB) {
  65. if (matchA.end < matchB.start) {
  66. return false; // A ends before B starts
  67. }
  68. if (matchB.end < matchA.start) {
  69. return false; // B ends before A starts
  70. }
  71. return true;
  72. }
  73. /*
  74. * If a query is wrapped in quotes, the user does not want to
  75. * use fuzzy search for this query.
  76. */
  77. function queryExpectsExactMatch(query) {
  78. return query.startsWith('"') && query.endsWith('"');
  79. }
  80. /**
  81. * Helper function to prepare a search value for scoring by removing unwanted characters
  82. * and allowing to score on multiple pieces separated by whitespace character.
  83. */
  84. const MULTIPLE_QUERY_VALUES_SEPARATOR = ' ';
  85. export function prepareQuery(original) {
  86. if (typeof original !== 'string') {
  87. original = '';
  88. }
  89. const originalLowercase = original.toLowerCase();
  90. const { pathNormalized, normalized, normalizedLowercase } = normalizeQuery(original);
  91. const containsPathSeparator = pathNormalized.indexOf(sep) >= 0;
  92. const expectExactMatch = queryExpectsExactMatch(original);
  93. let values = undefined;
  94. const originalSplit = original.split(MULTIPLE_QUERY_VALUES_SEPARATOR);
  95. if (originalSplit.length > 1) {
  96. for (const originalPiece of originalSplit) {
  97. const expectExactMatchPiece = queryExpectsExactMatch(originalPiece);
  98. const { pathNormalized: pathNormalizedPiece, normalized: normalizedPiece, normalizedLowercase: normalizedLowercasePiece } = normalizeQuery(originalPiece);
  99. if (normalizedPiece) {
  100. if (!values) {
  101. values = [];
  102. }
  103. values.push({
  104. original: originalPiece,
  105. originalLowercase: originalPiece.toLowerCase(),
  106. pathNormalized: pathNormalizedPiece,
  107. normalized: normalizedPiece,
  108. normalizedLowercase: normalizedLowercasePiece,
  109. expectContiguousMatch: expectExactMatchPiece
  110. });
  111. }
  112. }
  113. }
  114. return { original, originalLowercase, pathNormalized, normalized, normalizedLowercase, values, containsPathSeparator, expectContiguousMatch: expectExactMatch };
  115. }
  116. function normalizeQuery(original) {
  117. let pathNormalized;
  118. if (isWindows) {
  119. pathNormalized = original.replace(/\//g, sep); // Help Windows users to search for paths when using slash
  120. }
  121. else {
  122. pathNormalized = original.replace(/\\/g, sep); // Help macOS/Linux users to search for paths when using backslash
  123. }
  124. // we remove quotes here because quotes are used for exact match search
  125. const normalized = stripWildcards(pathNormalized).replace(/\s|"/g, '');
  126. return {
  127. pathNormalized,
  128. normalized,
  129. normalizedLowercase: normalized.toLowerCase()
  130. };
  131. }
  132. export function pieceToQuery(arg1) {
  133. if (Array.isArray(arg1)) {
  134. return prepareQuery(arg1.map(piece => piece.original).join(MULTIPLE_QUERY_VALUES_SEPARATOR));
  135. }
  136. return prepareQuery(arg1.original);
  137. }
  138. //#endregion