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