iconLabels.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  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 { CSSIcon } from './codicons.js';
  6. import { matchesFuzzy } from './filters.js';
  7. import { ltrim } from './strings.js';
  8. export const iconStartMarker = '$(';
  9. const iconsRegex = new RegExp(`\\$\\(${CSSIcon.iconNameExpression}(?:${CSSIcon.iconModifierExpression})?\\)`, 'g'); // no capturing groups
  10. const iconNameCharacterRegexp = new RegExp(CSSIcon.iconNameCharacter);
  11. const escapeIconsRegex = new RegExp(`(\\\\)?${iconsRegex.source}`, 'g');
  12. export function escapeIcons(text) {
  13. return text.replace(escapeIconsRegex, (match, escaped) => escaped ? match : `\\${match}`);
  14. }
  15. const markdownEscapedIconsRegex = new RegExp(`\\\\${iconsRegex.source}`, 'g');
  16. export function markdownEscapeEscapedIcons(text) {
  17. // Need to add an extra \ for escaping in markdown
  18. return text.replace(markdownEscapedIconsRegex, match => `\\${match}`);
  19. }
  20. const stripIconsRegex = new RegExp(`(\\s)?(\\\\)?${iconsRegex.source}(\\s)?`, 'g');
  21. export function stripIcons(text) {
  22. if (text.indexOf(iconStartMarker) === -1) {
  23. return text;
  24. }
  25. return text.replace(stripIconsRegex, (match, preWhitespace, escaped, postWhitespace) => escaped ? match : preWhitespace || postWhitespace || '');
  26. }
  27. export function parseLabelWithIcons(text) {
  28. const firstIconIndex = text.indexOf(iconStartMarker);
  29. if (firstIconIndex === -1) {
  30. return { text }; // return early if the word does not include an icon
  31. }
  32. return doParseLabelWithIcons(text, firstIconIndex);
  33. }
  34. function doParseLabelWithIcons(text, firstIconIndex) {
  35. const iconOffsets = [];
  36. let textWithoutIcons = '';
  37. function appendChars(chars) {
  38. if (chars) {
  39. textWithoutIcons += chars;
  40. for (const _ of chars) {
  41. iconOffsets.push(iconsOffset); // make sure to fill in icon offsets
  42. }
  43. }
  44. }
  45. let currentIconStart = -1;
  46. let currentIconValue = '';
  47. let iconsOffset = 0;
  48. let char;
  49. let nextChar;
  50. let offset = firstIconIndex;
  51. const length = text.length;
  52. // Append all characters until the first icon
  53. appendChars(text.substr(0, firstIconIndex));
  54. // example: $(file-symlink-file) my cool $(other-icon) entry
  55. while (offset < length) {
  56. char = text[offset];
  57. nextChar = text[offset + 1];
  58. // beginning of icon: some value $( <--
  59. if (char === iconStartMarker[0] && nextChar === iconStartMarker[1]) {
  60. currentIconStart = offset;
  61. // if we had a previous potential icon value without
  62. // the closing ')', it was actually not an icon and
  63. // so we have to add it to the actual value
  64. appendChars(currentIconValue);
  65. currentIconValue = iconStartMarker;
  66. offset++; // jump over '('
  67. }
  68. // end of icon: some value $(some-icon) <--
  69. else if (char === ')' && currentIconStart !== -1) {
  70. const currentIconLength = offset - currentIconStart + 1; // +1 to include the closing ')'
  71. iconsOffset += currentIconLength;
  72. currentIconStart = -1;
  73. currentIconValue = '';
  74. }
  75. // within icon
  76. else if (currentIconStart !== -1) {
  77. // Make sure this is a real icon name
  78. if (iconNameCharacterRegexp.test(char)) {
  79. currentIconValue += char;
  80. }
  81. else {
  82. // This is not a real icon, treat it as text
  83. appendChars(currentIconValue);
  84. currentIconStart = -1;
  85. currentIconValue = '';
  86. }
  87. }
  88. // any value outside of icon
  89. else {
  90. appendChars(char);
  91. }
  92. offset++;
  93. }
  94. // if we had a previous potential icon value without
  95. // the closing ')', it was actually not an icon and
  96. // so we have to add it to the actual value
  97. appendChars(currentIconValue);
  98. return { text: textWithoutIcons, iconOffsets };
  99. }
  100. export function matchesFuzzyIconAware(query, target, enableSeparateSubstringMatching = false) {
  101. const { text, iconOffsets } = target;
  102. // Return early if there are no icon markers in the word to match against
  103. if (!iconOffsets || iconOffsets.length === 0) {
  104. return matchesFuzzy(query, text, enableSeparateSubstringMatching);
  105. }
  106. // Trim the word to match against because it could have leading
  107. // whitespace now if the word started with an icon
  108. const wordToMatchAgainstWithoutIconsTrimmed = ltrim(text, ' ');
  109. const leadingWhitespaceOffset = text.length - wordToMatchAgainstWithoutIconsTrimmed.length;
  110. // match on value without icon
  111. const matches = matchesFuzzy(query, wordToMatchAgainstWithoutIconsTrimmed, enableSeparateSubstringMatching);
  112. // Map matches back to offsets with icon and trimming
  113. if (matches) {
  114. for (const match of matches) {
  115. const iconOffset = iconOffsets[match.start + leadingWhitespaceOffset] /* icon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */;
  116. match.start += iconOffset;
  117. match.end += iconOffset;
  118. }
  119. }
  120. return matches;
  121. }