iconLabels.js 5.3 KB

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