• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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