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