namespace ts { export enum PatternMatchKind { exact , prefix , substring , camelCase } export interface PatternMatch { kind : PatternMatchKind ; isCaseSensitive : boolean ; } export interface PatternMatcher { getMatchForLastSegmentOfPattern ( candidate ) : PatternMatch | undefined ; getFullMatch ( candidateContainers , candidate ) : PatternMatch | undefined ; patternContainsDots : boolean ; } interface Segment { totalTextChunk : TextChunk ; subWordTextChunks : TextChunk [ ] ; } interface TextChunk { text : string ; textLowerCase : string ; isLowerCase : boolean ; characterSpans : TextSpan [ ] ; } function createPatternMatch ( kind , isCaseSensitive ) { return { kind , isCaseSensitive } ; } export function createPatternMatcher ( pattern ) { const stringToWordSpans = new Map < string , TextSpan [ ] > ( ) ; const dotSeparatedSegments = pattern . trim ( ) . split ( "." ) . map ( p => createSegment ( p . trim ( ) ) ) ; if ( dotSeparatedSegments . some ( segment => ! segment . subWordTextChunks . length ) ) return undefined ; return { getFullMatch : ( containers , candidate ) => getFullMatch ( containers , candidate , dotSeparatedSegments , stringToWordSpans ) , getMatchForLastSegmentOfPattern : candidate => matchSegment ( candidate , last ( dotSeparatedSegments ) , stringToWordSpans ) , patternContainsDots : dotSeparatedSegments . length > 1 } ; } function getFullMatch ( candidateContainers , candidate , dotSeparatedSegments , stringToWordSpans ) { const candidateMatch = matchSegment ( candidate , last ( dotSeparatedSegments ) , stringToWordSpans ) ; if ( ! candidateMatch ) { return undefined ; } if ( dotSeparatedSegments . length - 1 > candidateContainers . length ) { return undefined ; } let bestMatch ; for ( let i = dotSeparatedSegments . length - 2 , j = candidateContainers . length - 1 ; i >= 0 ; i -= 1 , j -= 1 ) { bestMatch = betterMatch ( bestMatch , matchSegment ( candidateContainers [ j ] , dotSeparatedSegments [ i ] , stringToWordSpans ) ) ; } return bestMatch ; } function getWordSpans ( word , stringToWordSpans ) { let spans = stringToWordSpans . get ( word ) ; if ( ! spans ) { stringToWordSpans . set ( word , spans = breakIntoWordSpans ( word ) ) ; } return spans ; } function matchTextChunk ( candidate , chunk , stringToWordSpans ) { const index = indexOfIgnoringCase ( candidate , chunk . textLowerCase ) ; if ( index === 0 ) { return createPatternMatch ( chunk . text . length === candidate . length ? PatternMatchKind . exact : PatternMatchKind . prefix , startsWith ( candidate , chunk . text ) ) ; } if ( chunk . isLowerCase ) { if ( index === - 1 ) return undefined ; const wordSpans = getWordSpans ( candidate , stringToWordSpans ) ; for ( const span of wordSpans ) { if ( partStartsWith ( candidate , span , chunk . text , true ) ) { return createPatternMatch ( PatternMatchKind . substring , partStartsWith ( candidate , span , chunk . text , false ) ) ; } } if ( chunk . text . length < candidate . length && isUpperCaseLetter ( candidate . charCodeAt ( index ) ) ) { return createPatternMatch ( PatternMatchKind . substring , false ) ; } } else { if ( candidate . indexOf ( chunk . text ) > 0 ) { return createPatternMatch ( PatternMatchKind . substring , true ) ; } if ( chunk . characterSpans . length > 0 ) { const candidateParts = getWordSpans ( candidate , stringToWordSpans ) ; const isCaseSensitive = tryCamelCaseMatch ( candidate , candidateParts , chunk , false ) ? true : tryCamelCaseMatch ( candidate , candidateParts , chunk , true ) ? false : undefined ; if ( isCaseSensitive !== undefined ) { return createPatternMatch ( PatternMatchKind . camelCase , isCaseSensitive ) ; } } } } function matchSegment ( candidate , segment , stringToWordSpans ) { if ( every ( segment . totalTextChunk . text , ch => ch !== CharacterCodes . space && ch !== CharacterCodes . asterisk ) ) { const match = matchTextChunk ( candidate , segment . totalTextChunk , stringToWordSpans ) ; if ( match ) return match ; } const subWordTextChunks = segment . subWordTextChunks ; let bestMatch ; for ( const subWordTextChunk of subWordTextChunks ) { bestMatch = betterMatch ( bestMatch , matchTextChunk ( candidate , subWordTextChunk , stringToWordSpans ) ) ; } return bestMatch ; } function betterMatch ( a , b ) { return min ( a , b , compareMatches ) ; } function compareMatches ( a , b ) { return a === undefined ? Comparison . GreaterThan : b === undefined ? Comparison . LessThan : compareValues ( a . kind , b . kind ) || compareBooleans ( ! a . isCaseSensitive , ! b . isCaseSensitive ) ; } function partStartsWith ( candidate , candidateSpan , pattern , ignoreCase , patternSpan = { start : 0 , length : pattern . length } ) { return patternSpan . length <= candidateSpan . length && everyInRange ( 0 , patternSpan . length , i => equalChars ( pattern . charCodeAt ( patternSpan . start + i ) , candidate . charCodeAt ( candidateSpan . start + i ) , ignoreCase ) ) ; } function equalChars ( ch1 , ch2 , ignoreCase ) { return ignoreCase ? toLowerCase ( ch1 ) === toLowerCase ( ch2 ) : ch1 === ch2 ; } function tryCamelCaseMatch ( candidate , candidateParts , chunk , ignoreCase ) { const chunkCharacterSpans = chunk . characterSpans ; let currentCandidate = 0 ; let currentChunkSpan = 0 ; let firstMatch ; let contiguous ; while ( true ) { if ( currentChunkSpan === chunkCharacterSpans . length ) { return true ; } else if ( currentCandidate === candidateParts . length ) { return false ; } let candidatePart = candidateParts [ currentCandidate ] ; let gotOneMatchThisCandidate = false ; for ( ; currentChunkSpan < chunkCharacterSpans . length ; currentChunkSpan ++ ) { const chunkCharacterSpan = chunkCharacterSpans [ currentChunkSpan ] ; if ( gotOneMatchThisCandidate ) { if ( ! isUpperCaseLetter ( chunk . text . charCodeAt ( chunkCharacterSpans [ currentChunkSpan - 1 ] . start ) ) || ! isUpperCaseLetter ( chunk . text . charCodeAt ( chunkCharacterSpans [ currentChunkSpan ] . start ) ) ) { break ; } } if ( ! partStartsWith ( candidate , candidatePart , chunk . text , ignoreCase , chunkCharacterSpan ) ) { break ; } gotOneMatchThisCandidate = true ; firstMatch = firstMatch === undefined ? currentCandidate : firstMatch ; contiguous = contiguous === undefined ? true : contiguous ; candidatePart = createTextSpan ( candidatePart . start + chunkCharacterSpan . length , candidatePart . length - chunkCharacterSpan . length ) ; } if ( ! gotOneMatchThisCandidate && contiguous !== undefined ) { contiguous = false ; } currentCandidate ++ ; } } function createSegment ( text ) { return { totalTextChunk : createTextChunk ( text ) , subWordTextChunks : breakPatternIntoTextChunks ( text ) } ; } function isUpperCaseLetter ( ch ) { if ( ch >= CharacterCodes . A && ch <= CharacterCodes . Z ) { return true ; } if ( ch < CharacterCodes . maxAsciiCharacter || ! isUnicodeIdentifierStart ( ch , ScriptTarget . Latest ) ) { return false ; } const str = String . fromCharCode ( ch ) ; return str === str . toUpperCase ( ) ; } function isLowerCaseLetter ( ch ) { if ( ch >= CharacterCodes . a && ch <= CharacterCodes . z ) { return true ; } if ( ch < CharacterCodes . maxAsciiCharacter || ! isUnicodeIdentifierStart ( ch , ScriptTarget . Latest ) ) { return false ; } const str = String . fromCharCode ( ch ) ; return str === str . toLowerCase ( ) ; } function indexOfIgnoringCase ( str , value ) { const n = str . length - value . length ; for ( let start = 0 ; start <= n ; start ++ ) { if ( every ( value , ( valueChar , i ) => toLowerCase ( str . charCodeAt ( i + start ) ) === valueChar ) ) { return start ; } } return - 1 ; } function toLowerCase ( ch ) { if ( ch >= CharacterCodes . A && ch <= CharacterCodes . Z ) { return CharacterCodes . a + ( ch - CharacterCodes . A ) ; } if ( ch < CharacterCodes . maxAsciiCharacter ) { return ch ; } return String . fromCharCode ( ch ) . toLowerCase ( ) . charCodeAt ( 0 ) ; } function isDigit ( ch ) { return ch >= CharacterCodes . _0 && ch <= CharacterCodes . _9 ; } function isWordChar ( ch ) { return isUpperCaseLetter ( ch ) || isLowerCaseLetter ( ch ) || isDigit ( ch ) || ch === CharacterCodes . _ || ch === CharacterCodes . $ ; } function breakPatternIntoTextChunks ( pattern ) { const result = [ ] ; let wordStart = 0 ; let wordLength = 0 ; for ( let i = 0 ; i < pattern . length ; i ++ ) { const ch = pattern . charCodeAt ( i ) ; if ( isWordChar ( ch ) ) { if ( wordLength === 0 ) { wordStart = i ; } wordLength ++ ; } else { if ( wordLength > 0 ) { result . push ( createTextChunk ( pattern . substr ( wordStart , wordLength ) ) ) ; wordLength = 0 ; } } } if ( wordLength > 0 ) { result . push ( createTextChunk ( pattern . substr ( wordStart , wordLength ) ) ) ; } return result ; } function createTextChunk ( text ) { const textLowerCase = text . toLowerCase ( ) ; return { text , textLowerCase , isLowerCase : text === textLowerCase , characterSpans : breakIntoCharacterSpans ( text ) } ; } export function breakIntoCharacterSpans ( identifier ) { return breakIntoSpans ( identifier , false ) ; } export function breakIntoWordSpans ( identifier ) { return breakIntoSpans ( identifier , true ) ; } function breakIntoSpans ( identifier , word ) { const result = [ ] ; let wordStart = 0 ; for ( let i = 1 ; i < identifier . length ; i ++ ) { const lastIsDigit = isDigit ( identifier . charCodeAt ( i - 1 ) ) ; const currentIsDigit = isDigit ( identifier . charCodeAt ( i ) ) ; const hasTransitionFromLowerToUpper = transitionFromLowerToUpper ( identifier , word , i ) ; const hasTransitionFromUpperToLower = word && transitionFromUpperToLower ( identifier , i , wordStart ) ; if ( charIsPunctuation ( identifier . charCodeAt ( i - 1 ) ) || charIsPunctuation ( identifier . charCodeAt ( i ) ) || lastIsDigit !== currentIsDigit || hasTransitionFromLowerToUpper || hasTransitionFromUpperToLower ) { if ( ! isAllPunctuation ( identifier , wordStart , i ) ) { result . push ( createTextSpan ( wordStart , i - wordStart ) ) ; } wordStart = i ; } } if ( ! isAllPunctuation ( identifier , wordStart , identifier . length ) ) { result . push ( createTextSpan ( wordStart , identifier . length - wordStart ) ) ; } return result ; } function charIsPunctuation ( ch ) { switch ( ch ) { case CharacterCodes . exclamation : case CharacterCodes . doubleQuote : case CharacterCodes . hash : case CharacterCodes . percent : case CharacterCodes . ampersand : case CharacterCodes . singleQuote : case CharacterCodes . openParen : case CharacterCodes . closeParen : case CharacterCodes . asterisk : case CharacterCodes . comma : case CharacterCodes . minus : case CharacterCodes . dot : case CharacterCodes . slash : case CharacterCodes . colon : case CharacterCodes . semicolon : case CharacterCodes . question : case CharacterCodes . at : case CharacterCodes . openBracket : case CharacterCodes . backslash : case CharacterCodes . closeBracket : case CharacterCodes . _ : case CharacterCodes . openBrace : case CharacterCodes . closeBrace : return true ; } return false ; } function isAllPunctuation ( identifier , start , end ) { return every ( identifier , ch => charIsPunctuation ( ch ) && ch !== CharacterCodes . _ , start , end ) ; } function transitionFromUpperToLower ( identifier , index , wordStart ) { return index !== wordStart && index + 1 < identifier . length && isUpperCaseLetter ( identifier . charCodeAt ( index ) ) && isLowerCaseLetter ( identifier . charCodeAt ( index + 1 ) ) && every ( identifier , isUpperCaseLetter , wordStart , index ) ; } function transitionFromLowerToUpper ( identifier , word , index ) { const lastIsUpper = isUpperCaseLetter ( identifier . charCodeAt ( index - 1 ) ) ; const currentIsUpper = isUpperCaseLetter ( identifier . charCodeAt ( index ) ) ; return currentIsUpper && ( ! word || ! lastIsUpper ) ; } function everyInRange ( start , end , pred ) { for ( let i = start ; i < end ; i ++ ) { if ( ! pred ( i ) ) { return false ; } } return true ; } function every ( s , pred , start = 0 , end = s . length ) { return everyInRange ( start , end , i => pred ( s . charCodeAt ( i ) , i ) ) ; } }