1import { 2 CharacterCodes, compareBooleans, compareValues, Comparison, createTextSpan, ESMap, isUnicodeIdentifierStart, last, 3 Map, min, ScriptTarget, startsWith, TextSpan, 4} from "./_namespaces/ts"; 5 6// Note(cyrusn): this enum is ordered from strongest match type to weakest match type. 7/** @internal */ 8export enum PatternMatchKind { 9 exact, 10 prefix, 11 substring, 12 camelCase 13} 14 15// Information about a match made by the pattern matcher between a candidate and the 16// search pattern. 17/** @internal */ 18export interface PatternMatch { 19 // What kind of match this was. Exact matches are better than prefix matches which are 20 // better than substring matches which are better than CamelCase matches. 21 kind: PatternMatchKind; 22 23 // If this was a match where all constituent parts of the candidate and search pattern 24 // matched case sensitively or case insensitively. Case sensitive matches of the kind 25 // are better matches than insensitive matches. 26 isCaseSensitive: boolean; 27} 28 29// The pattern matcher maintains an internal cache of information as it is used. Therefore, 30// you should not keep it around forever and should get and release the matcher appropriately 31// once you no longer need it. 32/** @internal */ 33export interface PatternMatcher { 34 // Used to match a candidate against the last segment of a possibly dotted pattern. This 35 // is useful as a quick check to prevent having to compute a container before calling 36 // "getMatches". 37 // 38 // For example, if the search pattern is "ts.c.SK" and the candidate is "SyntaxKind", then 39 // this will return a successful match, having only tested "SK" against "SyntaxKind". At 40 // that point a call can be made to 'getMatches("SyntaxKind", "ts.compiler")', with the 41 // work to create 'ts.compiler' only being done once the first match succeeded. 42 getMatchForLastSegmentOfPattern(candidate: string): PatternMatch | undefined; 43 44 // Fully checks a candidate, with an dotted container, against the search pattern. 45 // The candidate must match the last part of the search pattern, and the dotted container 46 // must match the preceding segments of the pattern. 47 getFullMatch(candidateContainers: readonly string[], candidate: string): PatternMatch | undefined; 48 49 // Whether or not the pattern contained dots or not. Clients can use this to determine 50 // If they should call getMatches, or if getMatchesForLastSegmentOfPattern is sufficient. 51 patternContainsDots: boolean; 52} 53 54// First we break up the pattern given by dots. Each portion of the pattern between the 55// dots is a 'Segment'. The 'Segment' contains information about the entire section of 56// text between the dots, as well as information about any individual 'Words' that we 57// can break the segment into. A 'Word' is simply a contiguous sequence of characters 58// that can appear in a typescript identifier. So "GetKeyword" would be one word, while 59// "Get Keyword" would be two words. Once we have the individual 'words', we break those 60// into constituent 'character spans' of interest. For example, while 'UIElement' is one 61// word, it make character spans corresponding to "U", "I" and "Element". These spans 62// are then used when doing camel cased matches against candidate patterns. 63interface Segment { 64 // Information about the entire piece of text between the dots. For example, if the 65 // text between the dots is 'GetKeyword', then TotalTextChunk.Text will be 'GetKeyword' and 66 // TotalTextChunk.CharacterSpans will correspond to 'Get', 'Keyword'. 67 totalTextChunk: TextChunk; 68 69 // Information about the subwords compromising the total word. For example, if the 70 // text between the dots is 'GetFoo KeywordBar', then the subwords will be 'GetFoo' 71 // and 'KeywordBar'. Those individual words will have CharacterSpans of ('Get' and 72 // 'Foo') and('Keyword' and 'Bar') respectively. 73 subWordTextChunks: TextChunk[]; 74} 75 76// Information about a chunk of text from the pattern. The chunk is a piece of text, with 77// cached information about the character spans within in. Character spans are used for 78// camel case matching. 79interface TextChunk { 80 // The text of the chunk. This should be a contiguous sequence of character that could 81 // occur in a symbol name. 82 text: string; 83 84 // The text of a chunk in lower case. Cached because it is needed often to check for 85 // case insensitive matches. 86 textLowerCase: string; 87 88 // Whether or not this chunk is entirely lowercase. We have different rules when searching 89 // for something entirely lowercase or not. 90 isLowerCase: boolean; 91 92 // The spans in this text chunk that we think are of interest and should be matched 93 // independently. For example, if the chunk is for "UIElement" the the spans of interest 94 // correspond to "U", "I" and "Element". If "UIElement" isn't found as an exact, prefix. 95 // or substring match, then the character spans will be used to attempt a camel case match. 96 characterSpans: TextSpan[]; 97} 98 99function createPatternMatch(kind: PatternMatchKind, isCaseSensitive: boolean): PatternMatch { 100 return { 101 kind, 102 isCaseSensitive 103 }; 104} 105 106/** @internal */ 107export function createPatternMatcher(pattern: string): PatternMatcher | undefined { 108 // We'll often see the same candidate string many times when searching (For example, when 109 // we see the name of a module that is used everywhere, or the name of an overload). As 110 // such, we cache the information we compute about the candidate for the life of this 111 // pattern matcher so we don't have to compute it multiple times. 112 const stringToWordSpans = new Map<string, TextSpan[]>(); 113 114 const dotSeparatedSegments = pattern.trim().split(".").map(p => createSegment(p.trim())); 115 // A segment is considered invalid if we couldn't find any words in it. 116 if (dotSeparatedSegments.some(segment => !segment.subWordTextChunks.length)) return undefined; 117 118 return { 119 getFullMatch: (containers, candidate) => getFullMatch(containers, candidate, dotSeparatedSegments, stringToWordSpans), 120 getMatchForLastSegmentOfPattern: candidate => matchSegment(candidate, last(dotSeparatedSegments), stringToWordSpans), 121 patternContainsDots: dotSeparatedSegments.length > 1 122 }; 123} 124 125function getFullMatch(candidateContainers: readonly string[], candidate: string, dotSeparatedSegments: readonly Segment[], stringToWordSpans: ESMap<string, TextSpan[]>): PatternMatch | undefined { 126 // First, check that the last part of the dot separated pattern matches the name of the 127 // candidate. If not, then there's no point in proceeding and doing the more 128 // expensive work. 129 const candidateMatch = matchSegment(candidate, last(dotSeparatedSegments), stringToWordSpans); 130 if (!candidateMatch) { 131 return undefined; 132 } 133 134 // -1 because the last part was checked against the name, and only the rest 135 // of the parts are checked against the container. 136 if (dotSeparatedSegments.length - 1 > candidateContainers.length) { 137 // There weren't enough container parts to match against the pattern parts. 138 // So this definitely doesn't match. 139 return undefined; 140 } 141 142 let bestMatch: PatternMatch | undefined; 143 for (let i = dotSeparatedSegments.length - 2, j = candidateContainers.length - 1; 144 i >= 0; 145 i -= 1, j -= 1) { 146 bestMatch = betterMatch(bestMatch, matchSegment(candidateContainers[j], dotSeparatedSegments[i], stringToWordSpans)); 147 } 148 return bestMatch; 149} 150 151function getWordSpans(word: string, stringToWordSpans: ESMap<string, TextSpan[]>): TextSpan[] { 152 let spans = stringToWordSpans.get(word); 153 if (!spans) { 154 stringToWordSpans.set(word, spans = breakIntoWordSpans(word)); 155 } 156 return spans; 157} 158 159function matchTextChunk(candidate: string, chunk: TextChunk, stringToWordSpans: ESMap<string, TextSpan[]>): PatternMatch | undefined { 160 const index = indexOfIgnoringCase(candidate, chunk.textLowerCase); 161 if (index === 0) { 162 // a) Check if the word is a prefix of the candidate, in a case insensitive or 163 // sensitive manner. If it does, return that there was an exact match if the word and candidate are the same length, else a prefix match. 164 return createPatternMatch(chunk.text.length === candidate.length ? PatternMatchKind.exact : PatternMatchKind.prefix, /*isCaseSensitive:*/ startsWith(candidate, chunk.text)); 165 } 166 167 if (chunk.isLowerCase) { 168 if (index === -1) return undefined; 169 // b) If the part is entirely lowercase, then check if it is contained anywhere in the 170 // candidate in a case insensitive manner. If so, return that there was a substring 171 // match. 172 // 173 // Note: We only have a substring match if the lowercase part is prefix match of some 174 // word part. That way we don't match something like 'Class' when the user types 'a'. 175 // But we would match 'FooAttribute' (since 'Attribute' starts with 'a'). 176 const wordSpans = getWordSpans(candidate, stringToWordSpans); 177 for (const span of wordSpans) { 178 if (partStartsWith(candidate, span, chunk.text, /*ignoreCase:*/ true)) { 179 return createPatternMatch(PatternMatchKind.substring, /*isCaseSensitive:*/ partStartsWith(candidate, span, chunk.text, /*ignoreCase:*/ false)); 180 } 181 } 182 // c) Is the pattern a substring of the candidate starting on one of the candidate's word boundaries? 183 // We could check every character boundary start of the candidate for the pattern. However, that's 184 // an m * n operation in the wost case. Instead, find the first instance of the pattern 185 // substring, and see if it starts on a capital letter. It seems unlikely that the user will try to 186 // filter the list based on a substring that starts on a capital letter and also with a lowercase one. 187 // (Pattern: fogbar, Candidate: quuxfogbarFogBar). 188 if (chunk.text.length < candidate.length && isUpperCaseLetter(candidate.charCodeAt(index))) { 189 return createPatternMatch(PatternMatchKind.substring, /*isCaseSensitive:*/ false); 190 } 191 } 192 else { 193 // d) If the part was not entirely lowercase, then check if it is contained in the 194 // candidate in a case *sensitive* manner. If so, return that there was a substring 195 // match. 196 if (candidate.indexOf(chunk.text) > 0) { 197 return createPatternMatch(PatternMatchKind.substring, /*isCaseSensitive:*/ true); 198 } 199 // e) If the part was not entirely lowercase, then attempt a camel cased match as well. 200 if (chunk.characterSpans.length > 0) { 201 const candidateParts = getWordSpans(candidate, stringToWordSpans); 202 const isCaseSensitive = tryCamelCaseMatch(candidate, candidateParts, chunk, /*ignoreCase:*/ false) ? true 203 : tryCamelCaseMatch(candidate, candidateParts, chunk, /*ignoreCase:*/ true) ? false : undefined; 204 if (isCaseSensitive !== undefined) { 205 return createPatternMatch(PatternMatchKind.camelCase, isCaseSensitive); 206 } 207 } 208 } 209} 210 211function matchSegment(candidate: string, segment: Segment, stringToWordSpans: ESMap<string, TextSpan[]>): PatternMatch | undefined { 212 // First check if the segment matches as is. This is also useful if the segment contains 213 // characters we would normally strip when splitting into parts that we also may want to 214 // match in the candidate. For example if the segment is "@int" and the candidate is 215 // "@int", then that will show up as an exact match here. 216 // 217 // Note: if the segment contains a space or an asterisk then we must assume that it's a 218 // multi-word segment. 219 if (every(segment.totalTextChunk.text, ch => ch !== CharacterCodes.space && ch !== CharacterCodes.asterisk)) { 220 const match = matchTextChunk(candidate, segment.totalTextChunk, stringToWordSpans); 221 if (match) return match; 222 } 223 224 // The logic for pattern matching is now as follows: 225 // 226 // 1) Break the segment passed in into words. Breaking is rather simple and a 227 // good way to think about it that if gives you all the individual alphanumeric words 228 // of the pattern. 229 // 230 // 2) For each word try to match the word against the candidate value. 231 // 232 // 3) Matching is as follows: 233 // 234 // a) Check if the word is a prefix of the candidate, in a case insensitive or 235 // sensitive manner. If it does, return that there was an exact match if the word and candidate are the same length, else a prefix match. 236 // 237 // If the word is entirely lowercase: 238 // b) Then check if it is contained anywhere in the 239 // candidate in a case insensitive manner. If so, return that there was a substring 240 // match. 241 // 242 // Note: We only have a substring match if the lowercase part is prefix match of 243 // some word part. That way we don't match something like 'Class' when the user 244 // types 'a'. But we would match 'FooAttribute' (since 'Attribute' starts with 245 // 'a'). 246 // 247 // c) The word is all lower case. Is it a case insensitive substring of the candidate starting 248 // on a part boundary of the candidate? 249 // 250 // Else: 251 // d) If the word was not entirely lowercase, then check if it is contained in the 252 // candidate in a case *sensitive* manner. If so, return that there was a substring 253 // match. 254 // 255 // e) If the word was not entirely lowercase, then attempt a camel cased match as 256 // well. 257 // 258 // Only if all words have some sort of match is the pattern considered matched. 259 260 const subWordTextChunks = segment.subWordTextChunks; 261 let bestMatch: PatternMatch | undefined; 262 for (const subWordTextChunk of subWordTextChunks) { 263 bestMatch = betterMatch(bestMatch, matchTextChunk(candidate, subWordTextChunk, stringToWordSpans)); 264 } 265 return bestMatch; 266} 267 268function betterMatch(a: PatternMatch | undefined, b: PatternMatch | undefined): PatternMatch | undefined { 269 return min([a, b], compareMatches); 270} 271function compareMatches(a: PatternMatch | undefined, b: PatternMatch | undefined): Comparison { 272 return a === undefined ? Comparison.GreaterThan : b === undefined ? Comparison.LessThan 273 : compareValues(a.kind, b.kind) || compareBooleans(!a.isCaseSensitive, !b.isCaseSensitive); 274} 275 276function partStartsWith(candidate: string, candidateSpan: TextSpan, pattern: string, ignoreCase: boolean, patternSpan: TextSpan = { start: 0, length: pattern.length }): boolean { 277 return patternSpan.length <= candidateSpan.length // If pattern part is longer than the candidate part there can never be a match. 278 && everyInRange(0, patternSpan.length, i => equalChars(pattern.charCodeAt(patternSpan.start + i), candidate.charCodeAt(candidateSpan.start + i), ignoreCase)); 279} 280 281function equalChars(ch1: number, ch2: number, ignoreCase: boolean): boolean { 282 return ignoreCase ? toLowerCase(ch1) === toLowerCase(ch2) : ch1 === ch2; 283} 284 285function tryCamelCaseMatch(candidate: string, candidateParts: TextSpan[], chunk: TextChunk, ignoreCase: boolean): boolean { 286 const chunkCharacterSpans = chunk.characterSpans; 287 288 // Note: we may have more pattern parts than candidate parts. This is because multiple 289 // pattern parts may match a candidate part. For example "SiUI" against "SimpleUI". 290 // We'll have 3 pattern parts Si/U/I against two candidate parts Simple/UI. However, U 291 // and I will both match in UI. 292 293 let currentCandidate = 0; 294 let currentChunkSpan = 0; 295 let firstMatch: number | undefined; 296 let contiguous: boolean | undefined; 297 298 while (true) { 299 // Let's consider our termination cases 300 if (currentChunkSpan === chunkCharacterSpans.length) { 301 return true; 302 } 303 else if (currentCandidate === candidateParts.length) { 304 // No match, since we still have more of the pattern to hit 305 return false; 306 } 307 308 let candidatePart = candidateParts[currentCandidate]; 309 let gotOneMatchThisCandidate = false; 310 311 // Consider the case of matching SiUI against SimpleUIElement. The candidate parts 312 // will be Simple/UI/Element, and the pattern parts will be Si/U/I. We'll match 'Si' 313 // against 'Simple' first. Then we'll match 'U' against 'UI'. However, we want to 314 // still keep matching pattern parts against that candidate part. 315 for (; currentChunkSpan < chunkCharacterSpans.length; currentChunkSpan++) { 316 const chunkCharacterSpan = chunkCharacterSpans[currentChunkSpan]; 317 318 if (gotOneMatchThisCandidate) { 319 // We've already gotten one pattern part match in this candidate. We will 320 // only continue trying to consumer pattern parts if the last part and this 321 // part are both upper case. 322 if (!isUpperCaseLetter(chunk.text.charCodeAt(chunkCharacterSpans[currentChunkSpan - 1].start)) || 323 !isUpperCaseLetter(chunk.text.charCodeAt(chunkCharacterSpans[currentChunkSpan].start))) { 324 break; 325 } 326 } 327 328 if (!partStartsWith(candidate, candidatePart, chunk.text, ignoreCase, chunkCharacterSpan)) { 329 break; 330 } 331 332 gotOneMatchThisCandidate = true; 333 334 firstMatch = firstMatch === undefined ? currentCandidate : firstMatch; 335 336 // If we were contiguous, then keep that value. If we weren't, then keep that 337 // value. If we don't know, then set the value to 'true' as an initial match is 338 // obviously contiguous. 339 contiguous = contiguous === undefined ? true : contiguous; 340 341 candidatePart = createTextSpan(candidatePart.start + chunkCharacterSpan.length, candidatePart.length - chunkCharacterSpan.length); 342 } 343 344 // Check if we matched anything at all. If we didn't, then we need to unset the 345 // contiguous bit if we currently had it set. 346 // If we haven't set the bit yet, then that means we haven't matched anything so 347 // far, and we don't want to change that. 348 if (!gotOneMatchThisCandidate && contiguous !== undefined) { 349 contiguous = false; 350 } 351 352 // Move onto the next candidate. 353 currentCandidate++; 354 } 355} 356 357function createSegment(text: string): Segment { 358 return { 359 totalTextChunk: createTextChunk(text), 360 subWordTextChunks: breakPatternIntoTextChunks(text) 361 }; 362} 363 364function isUpperCaseLetter(ch: number) { 365 // Fast check for the ascii range. 366 if (ch >= CharacterCodes.A && ch <= CharacterCodes.Z) { 367 return true; 368 } 369 370 if (ch < CharacterCodes.maxAsciiCharacter || !isUnicodeIdentifierStart(ch, ScriptTarget.Latest)) { 371 return false; 372 } 373 374 // TODO: find a way to determine this for any unicode characters in a 375 // non-allocating manner. 376 const str = String.fromCharCode(ch); 377 return str === str.toUpperCase(); 378} 379 380function isLowerCaseLetter(ch: number) { 381 // Fast check for the ascii range. 382 if (ch >= CharacterCodes.a && ch <= CharacterCodes.z) { 383 return true; 384 } 385 386 if (ch < CharacterCodes.maxAsciiCharacter || !isUnicodeIdentifierStart(ch, ScriptTarget.Latest)) { 387 return false; 388 } 389 390 391 // TODO: find a way to determine this for any unicode characters in a 392 // non-allocating manner. 393 const str = String.fromCharCode(ch); 394 return str === str.toLowerCase(); 395} 396 397// Assumes 'value' is already lowercase. 398function indexOfIgnoringCase(str: string, value: string): number { 399 const n = str.length - value.length; 400 for (let start = 0; start <= n; start++) { 401 if (every(value, (valueChar, i) => toLowerCase(str.charCodeAt(i + start)) === valueChar)) { 402 return start; 403 } 404 } 405 406 return -1; 407} 408 409function toLowerCase(ch: number): number { 410 // Fast convert for the ascii range. 411 if (ch >= CharacterCodes.A && ch <= CharacterCodes.Z) { 412 return CharacterCodes.a + (ch - CharacterCodes.A); 413 } 414 415 if (ch < CharacterCodes.maxAsciiCharacter) { 416 return ch; 417 } 418 419 // TODO: find a way to compute this for any unicode characters in a 420 // non-allocating manner. 421 return String.fromCharCode(ch).toLowerCase().charCodeAt(0); 422} 423 424function isDigit(ch: number) { 425 // TODO(cyrusn): Find a way to support this for unicode digits. 426 return ch >= CharacterCodes._0 && ch <= CharacterCodes._9; 427} 428 429function isWordChar(ch: number) { 430 return isUpperCaseLetter(ch) || isLowerCaseLetter(ch) || isDigit(ch) || ch === CharacterCodes._ || ch === CharacterCodes.$; 431} 432 433function breakPatternIntoTextChunks(pattern: string): TextChunk[] { 434 const result: TextChunk[] = []; 435 let wordStart = 0; 436 let wordLength = 0; 437 438 for (let i = 0; i < pattern.length; i++) { 439 const ch = pattern.charCodeAt(i); 440 if (isWordChar(ch)) { 441 if (wordLength === 0) { 442 wordStart = i; 443 } 444 wordLength++; 445 } 446 else { 447 if (wordLength > 0) { 448 result.push(createTextChunk(pattern.substr(wordStart, wordLength))); 449 wordLength = 0; 450 } 451 } 452 } 453 454 if (wordLength > 0) { 455 result.push(createTextChunk(pattern.substr(wordStart, wordLength))); 456 } 457 458 return result; 459} 460 461function createTextChunk(text: string): TextChunk { 462 const textLowerCase = text.toLowerCase(); 463 return { 464 text, 465 textLowerCase, 466 isLowerCase: text === textLowerCase, 467 characterSpans: breakIntoCharacterSpans(text) 468 }; 469} 470 471/** @internal */ 472export function breakIntoCharacterSpans(identifier: string): TextSpan[] { 473 return breakIntoSpans(identifier, /*word:*/ false); 474} 475 476/** @internal */ 477export function breakIntoWordSpans(identifier: string): TextSpan[] { 478 return breakIntoSpans(identifier, /*word:*/ true); 479} 480 481function breakIntoSpans(identifier: string, word: boolean): TextSpan[] { 482 const result: TextSpan[] = []; 483 484 let wordStart = 0; 485 for (let i = 1; i < identifier.length; i++) { 486 const lastIsDigit = isDigit(identifier.charCodeAt(i - 1)); 487 const currentIsDigit = isDigit(identifier.charCodeAt(i)); 488 489 const hasTransitionFromLowerToUpper = transitionFromLowerToUpper(identifier, word, i); 490 const hasTransitionFromUpperToLower = word && transitionFromUpperToLower(identifier, i, wordStart); 491 492 if (charIsPunctuation(identifier.charCodeAt(i - 1)) || 493 charIsPunctuation(identifier.charCodeAt(i)) || 494 lastIsDigit !== currentIsDigit || 495 hasTransitionFromLowerToUpper || 496 hasTransitionFromUpperToLower) { 497 498 if (!isAllPunctuation(identifier, wordStart, i)) { 499 result.push(createTextSpan(wordStart, i - wordStart)); 500 } 501 502 wordStart = i; 503 } 504 } 505 506 if (!isAllPunctuation(identifier, wordStart, identifier.length)) { 507 result.push(createTextSpan(wordStart, identifier.length - wordStart)); 508 } 509 510 return result; 511} 512 513function charIsPunctuation(ch: number) { 514 switch (ch) { 515 case CharacterCodes.exclamation: 516 case CharacterCodes.doubleQuote: 517 case CharacterCodes.hash: 518 case CharacterCodes.percent: 519 case CharacterCodes.ampersand: 520 case CharacterCodes.singleQuote: 521 case CharacterCodes.openParen: 522 case CharacterCodes.closeParen: 523 case CharacterCodes.asterisk: 524 case CharacterCodes.comma: 525 case CharacterCodes.minus: 526 case CharacterCodes.dot: 527 case CharacterCodes.slash: 528 case CharacterCodes.colon: 529 case CharacterCodes.semicolon: 530 case CharacterCodes.question: 531 case CharacterCodes.at: 532 case CharacterCodes.openBracket: 533 case CharacterCodes.backslash: 534 case CharacterCodes.closeBracket: 535 case CharacterCodes._: 536 case CharacterCodes.openBrace: 537 case CharacterCodes.closeBrace: 538 return true; 539 } 540 541 return false; 542} 543 544function isAllPunctuation(identifier: string, start: number, end: number): boolean { 545 return every(identifier, ch => charIsPunctuation(ch) && ch !== CharacterCodes._, start, end); 546} 547 548function transitionFromUpperToLower(identifier: string, index: number, wordStart: number): boolean { 549 // Cases this supports: 550 // 1) IDisposable -> I, Disposable 551 // 2) UIElement -> UI, Element 552 // 3) HTMLDocument -> HTML, Document 553 // 554 // etc. 555 // We have a transition from an upper to a lower letter here. But we only 556 // want to break if all the letters that preceded are uppercase. i.e. if we 557 // have "Foo" we don't want to break that into "F, oo". But if we have 558 // "IFoo" or "UIFoo", then we want to break that into "I, Foo" and "UI, 559 // Foo". i.e. the last uppercase letter belongs to the lowercase letters 560 // that follows. Note: this will make the following not split properly: 561 // "HELLOthere". However, these sorts of names do not show up in .Net 562 // programs. 563 return index !== wordStart 564 && index + 1 < identifier.length 565 && isUpperCaseLetter(identifier.charCodeAt(index)) 566 && isLowerCaseLetter(identifier.charCodeAt(index + 1)) 567 && every(identifier, isUpperCaseLetter, wordStart, index); 568} 569 570function transitionFromLowerToUpper(identifier: string, word: boolean, index: number): boolean { 571 const lastIsUpper = isUpperCaseLetter(identifier.charCodeAt(index - 1)); 572 const currentIsUpper = isUpperCaseLetter(identifier.charCodeAt(index)); 573 574 // See if the casing indicates we're starting a new word. Note: if we're breaking on 575 // words, then just seeing an upper case character isn't enough. Instead, it has to 576 // be uppercase and the previous character can't be uppercase. 577 // 578 // For example, breaking "AddMetadata" on words would make: Add Metadata 579 // 580 // on characters would be: A dd M etadata 581 // 582 // Break "AM" on words would be: AM 583 // 584 // on characters would be: A M 585 // 586 // We break the search string on characters. But we break the symbol name on words. 587 return currentIsUpper && (!word || !lastIsUpper); 588} 589 590function everyInRange(start: number, end: number, pred: (n: number) => boolean): boolean { 591 for (let i = start; i < end; i++) { 592 if (!pred(i)) { 593 return false; 594 } 595 } 596 return true; 597} 598 599function every(s: string, pred: (ch: number, index: number) => boolean, start = 0, end = s.length): boolean { 600 return everyInRange(start, end, i => pred(s.charCodeAt(i), i)); 601} 602