/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { matchesFuzzy, IMatch } from 'vs/base/common/filters'; import { ltrim } from 'vs/base/common/strings'; export const iconStartMarker = '$('; const escapeIconsRegex = /(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi; export function escapeIcons(text: string): string { return text.replace(escapeIconsRegex, (match, escaped) => escaped ? match : `\\${match}`); } const markdownEscapedIconsRegex = /\\\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi; export function markdownEscapeEscapedIcons(text: string): string { // Need to add an extra \ for escaping in markdown return text.replace(markdownEscapedIconsRegex, match => `\\${match}`); } const markdownUnescapeIconsRegex = /(\\)?\$\\\(([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?)\\\)/gi; export function markdownUnescapeIcons(text: string): string { return text.replace(markdownUnescapeIconsRegex, (match, escaped, iconId) => escaped ? match : `$(${iconId})`); } const stripIconsRegex = /(\s)?(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)(\s)?/gi; export function stripIcons(text: string): string { if (text.indexOf(iconStartMarker) === -1) { return text; } return text.replace(stripIconsRegex, (match, preWhitespace, escaped, postWhitespace) => escaped ? match : preWhitespace || postWhitespace || ''); } export interface IParsedLabelWithIcons { readonly text: string; readonly iconOffsets?: readonly number[]; } export function parseLabelWithIcons(text: string): IParsedLabelWithIcons { const firstIconIndex = text.indexOf(iconStartMarker); if (firstIconIndex === -1) { return { text }; // return early if the word does not include an icon } return doParseLabelWithIcons(text, firstIconIndex); } function doParseLabelWithIcons(text: string, firstIconIndex: number): IParsedLabelWithIcons { const iconOffsets: number[] = []; let textWithoutIcons: string = ''; function appendChars(chars: string) { if (chars) { textWithoutIcons += chars; for (const _ of chars) { iconOffsets.push(iconsOffset); // make sure to fill in icon offsets } } } let currentIconStart = -1; let currentIconValue: string = ''; let iconsOffset = 0; let char: string; let nextChar: string; let offset = firstIconIndex; const length = text.length; // Append all characters until the first icon appendChars(text.substr(0, firstIconIndex)); // example: $(file-symlink-file) my cool $(other-icon) entry while (offset < length) { char = text[offset]; nextChar = text[offset + 1]; // beginning of icon: some value $( <-- if (char === iconStartMarker[0] && nextChar === iconStartMarker[1]) { currentIconStart = offset; // if we had a previous potential icon value without // the closing ')', it was actually not an icon and // so we have to add it to the actual value appendChars(currentIconValue); currentIconValue = iconStartMarker; offset++; // jump over '(' } // end of icon: some value $(some-icon) <-- else if (char === ')' && currentIconStart !== -1) { const currentIconLength = offset - currentIconStart + 1; // +1 to include the closing ')' iconsOffset += currentIconLength; currentIconStart = -1; currentIconValue = ''; } // within icon else if (currentIconStart !== -1) { // Make sure this is a real icon name if (/^[a-z0-9\-]$/i.test(char)) { currentIconValue += char; } else { // This is not a real icon, treat it as text appendChars(currentIconValue); currentIconStart = -1; currentIconValue = ''; } } // any value outside of icon else { appendChars(char); } offset++; } // if we had a previous potential icon value without // the closing ')', it was actually not an icon and // so we have to add it to the actual value appendChars(currentIconValue); return { text: textWithoutIcons, iconOffsets }; } export function matchesFuzzyIconAware(query: string, target: IParsedLabelWithIcons, enableSeparateSubstringMatching = false): IMatch[] | null { const { text, iconOffsets } = target; // Return early if there are no icon markers in the word to match against if (!iconOffsets || iconOffsets.length === 0) { return matchesFuzzy(query, text, enableSeparateSubstringMatching); } // Trim the word to match against because it could have leading // whitespace now if the word started with an icon const wordToMatchAgainstWithoutIconsTrimmed = ltrim(text, ' '); const leadingWhitespaceOffset = text.length - wordToMatchAgainstWithoutIconsTrimmed.length; // match on value without icon const matches = matchesFuzzy(query, wordToMatchAgainstWithoutIconsTrimmed, enableSeparateSubstringMatching); // Map matches back to offsets with icon and trimming if (matches) { for (const match of matches) { const iconOffset = iconOffsets[match.start + leadingWhitespaceOffset] /* icon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */; match.start += iconOffset; match.end += iconOffset; } } return matches; }