diff --git a/Jakefile b/Jakefile index acbc29b808e..ca1ee20a3ce 100644 --- a/Jakefile +++ b/Jakefile @@ -68,6 +68,7 @@ var servicesSources = [ "navigateTo.ts", "navigationBar.ts", "outliningElementsCollector.ts", + "patternMatcher.ts", "services.ts", "shims.ts", "signatureHelp.ts", @@ -139,7 +140,8 @@ var harnessSources = [ "incrementalParser.ts", "services/colorization.ts", "services/documentRegistry.ts", - "services/preProcessFile.ts" + "services/preProcessFile.ts", + "services/patternMatcher.ts" ].map(function (f) { return path.join(unittestsDirectory, f); })).concat([ diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 7d37801287a..311c746eafa 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -225,7 +225,7 @@ module ts { return false; } - function isUnicodeIdentifierStart(code: number, languageVersion: ScriptTarget) { + /* @internal */ export function isUnicodeIdentifierStart(code: number, languageVersion: ScriptTarget) { return languageVersion >= ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierStart) : lookupInUnicodeMap(code, unicodeES3IdentifierStart); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ccae04ae156..02627184fb7 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1646,6 +1646,7 @@ module ts { equals = 0x3D, // = exclamation = 0x21, // ! greaterThan = 0x3E, // > + hash = 0x23, // # lessThan = 0x3C, // < minus = 0x2D, // - openBrace = 0x7B, // { diff --git a/src/services/patternMatcher.ts b/src/services/patternMatcher.ts new file mode 100644 index 00000000000..da07c4400bc --- /dev/null +++ b/src/services/patternMatcher.ts @@ -0,0 +1,813 @@ +module ts { + // Note(cyrusn): this enum is ordered from strongest match type to weakest match type. + export enum PatternMatchKind { + Exact, + Prefix, + Substring, + CamelCase + } + + // Information about a match made by the pattern matcher between a candidate and the + // search pattern. + export interface PatternMatch { + // What kind of match this was. Exact matches are better than prefix matches which are + // better than substring matches which are better than CamelCase matches. + kind: PatternMatchKind; + + // If this was a camel case match, how strong the match is. Higher number means + // it was a better match. + camelCaseWeight?: number; + + // If this was a match where all constituent parts of the candidate and search pattern + // matched case sensitively or case insensitively. Case sensitive matches of the kind + // are better matches than insensitive matches. + isCaseSensitive: boolean; + + // Whether or not this match occurred with the punctuation from the search pattern stripped + // out or not. Matches without the punctuation stripped are better than ones with punctuation + // stripped. + punctuationStripped: boolean; + } + + // The pattern matcher maintains an internal cache of information as it is used. Therefore, + // you should not keep it around forever and should get and release the matcher appropriately + // once you no longer need it. + export interface PatternMatcher { + // Used to match a candidate against the last segment of a possibly dotted pattern. This + // is useful as a quick check to prevent having to compute a container before calling + // "getMatches". + // + // For example, if the search pattern is "ts.c.SK" and the candidate is "SyntaxKind", then + // this will return a successful match, having only tested "SK" against "SyntaxKind". At + // that point a call can be made to 'getMatches("SyntaxKind", "ts.compiler")', with the + // work to create 'ts.compiler' only being done once the first match succeeded. + getMatchesForLastSegmentOfPattern(candidate: string): PatternMatch[]; + + // Fully checks a candidate, with an dotted container, against the search pattern. + // The candidate must match the last part of the search pattern, and the dotted container + // must match the preceding segments of the pattern. + getMatches(candidate: string, dottedContainer: string): PatternMatch[]; + + // Whether or not the pattern contained dots or not. Clients can use this to determine + // If they should call getMatches, or if getMatchesForLastSegmentOfPattern is sufficient. + patternContainsDots: boolean; + } + + // First we break up the pattern given by dots. Each portion of the pattern between the + // dots is a 'Segment'. The 'Segment' contains information about the entire section of + // text between the dots, as well as information about any individual 'Words' that we + // can break the segment into. A 'Word' is simply a contiguous sequence of characters + // that can appear in a typescript identifier. So "GetKeyword" would be one word, while + // "Get Keyword" would be two words. Once we have the individual 'words', we break those + // into constituent 'character spans' of interest. For example, while 'UIElement' is one + // word, it make character spans corresponding to "U", "I" and "Element". These spans + // are then used when doing camel cased matches against candidate patterns. + interface Segment { + // Information about the entire piece of text between the dots. For example, if the + // text between the dots is 'GetKeyword', then TotalTextChunk.Text will be 'GetKeyword' and + // TotalTextChunk.CharacterSpans will correspond to 'Get', 'Keyword'. + totalTextChunk: TextChunk; + + // Information about the subwords compromising the total word. For example, if the + // text between the dots is 'GetFoo KeywordBar', then the subwords will be 'GetFoo' + // and 'KeywordBar'. Those individual words will have CharacterSpans of ('Get' and + // 'Foo') and('Keyword' and 'Bar') respectively. + subWordTextChunks: TextChunk[]; + } + + // Information about a chunk of text from the pattern. The chunk is a piece of text, with + // cached information about the character spans within in. Character spans are used for + // camel case matching. + interface TextChunk { + // The text of the chunk. This should be a contiguous sequence of character that could + // occur in a symbol name. + text: string; + + // The text of a chunk in lower case. Cached because it is needed often to check for + // case insensitive matches. + textLowerCase: string; + + // Whether or not this chunk is entirely lowercase. We have different rules when searching + // for something entirely lowercase or not. + isLowerCase: boolean; + + // The spans in this text chunk that we think are of interest and should be matched + // independently. For example, if the chunk is for "UIElement" the the spans of interest + // correspond to "U", "I" and "Element". If "UIElement" isn't found as an exaxt, prefix. + // or substring match, then the character spans will be used to attempt a camel case match. + characterSpans: TextSpan[]; + } + + function createPatternMatch(kind: PatternMatchKind, punctuationStripped: boolean, isCaseSensitive: boolean, camelCaseWeight?: number): PatternMatch { + return { + kind, + punctuationStripped, + isCaseSensitive, + camelCaseWeight + }; + } + + export function createPatternMatcher(pattern: string): PatternMatcher { + // We'll often see the same candidate string many times when searching (For example, when + // we see the name of a module that is used everywhere, or the name of an overload). As + // such, we cache the information we compute about the candidate for the life of this + // pattern matcher so we don't have to compute it multiple times. + var stringToWordSpans: Map = {}; + + pattern = pattern.trim(); + + var fullPatternSegment = createSegment(pattern); + var dotSeparatedSegments = pattern.split(".").map(p => createSegment(p.trim())); + var invalidPattern = dotSeparatedSegments.length === 0 || forEach(dotSeparatedSegments, segmentIsInvalid); + + return { + getMatches, + getMatchesForLastSegmentOfPattern, + patternContainsDots: dotSeparatedSegments.length > 1 + }; + + // Quick checks so we can bail out when asked to match a candidate. + function skipMatch(candidate: string) { + return invalidPattern || !candidate; + } + + function getMatchesForLastSegmentOfPattern(candidate: string): PatternMatch[] { + if (skipMatch(candidate)) { + return undefined; + } + + return matchSegment(candidate, lastOrUndefined(dotSeparatedSegments)); + } + + function getMatches(candidate: string, dottedContainer: string): PatternMatch[] { + if (skipMatch(candidate)) { + return undefined; + } + + // First, check that the last part of the dot separated pattern matches the name of the + // candidate. If not, then there's no point in proceeding and doing the more + // expensive work. + var candidateMatch = matchSegment(candidate, lastOrUndefined(dotSeparatedSegments)); + if (!candidateMatch) { + return undefined; + } + + dottedContainer = dottedContainer || ""; + var containerParts = dottedContainer.split("."); + + // -1 because the last part was checked against the name, and only the rest + // of the parts are checked against the container. + if (dotSeparatedSegments.length - 1 > containerParts.length) { + // There weren't enough container parts to match against the pattern parts. + // So this definitely doesn't match. + return null; + } + + // So far so good. Now break up the container for the candidate and check if all + // the dotted parts match up correctly. + var totalMatch = candidateMatch; + + for (var i = dotSeparatedSegments.length - 2, j = containerParts.length - 1; + i >= 0; + i--, j--) { + + var segment = dotSeparatedSegments[i]; + var containerName = containerParts[j]; + + var containerMatch = matchSegment(containerName, segment); + if (!containerMatch) { + // This container didn't match the pattern piece. So there's no match at all. + return undefined; + } + + addRange(totalMatch, containerMatch); + } + + // Success, this symbol's full name matched against the dotted name the user was asking + // about. + return totalMatch; + } + + function getWordSpans(word: string): TextSpan[] { + if (!hasProperty(stringToWordSpans, word)) { + stringToWordSpans[word] = breakIntoWordSpans(word); + } + + return stringToWordSpans[word]; + } + + function matchTextChunk(candidate: string, chunk: TextChunk, punctuationStripped: boolean): PatternMatch { + var index = indexOfIgnoringCase(candidate, chunk.textLowerCase); + if (index === 0) { + if (chunk.text.length === candidate.length) { + // a) Check if the part matches the candidate entirely, in an case insensitive or + // sensitive manner. If it does, return that there was an exact match. + return createPatternMatch(PatternMatchKind.Exact, punctuationStripped, /*isCaseSensitive:*/ candidate === chunk.text); + } + else { + // b) Check if the part is a prefix of the candidate, in a case insensitive or sensitive + // manner. If it does, return that there was a prefix match. + return createPatternMatch(PatternMatchKind.Prefix, punctuationStripped, /*isCaseSensitive:*/ startsWith(candidate, chunk.text)); + } + } + + var isLowercase = chunk.isLowerCase; + if (isLowercase) { + if (index > 0) { + // c) If the part is entirely lowercase, then check if it is contained anywhere in the + // candidate in a case insensitive manner. If so, return that there was a substring + // match. + // + // Note: We only have a substring match if the lowercase part is prefix match of some + // word part. That way we don't match something like 'Class' when the user types 'a'. + // But we would match 'FooAttribute' (since 'Attribute' starts with 'a'). + var wordSpans = getWordSpans(candidate); + for (var i = 0, n = wordSpans.length; i < n; i++) { + var span = wordSpans[i] + if (partStartsWith(candidate, span, chunk.text, /*ignoreCase:*/ true)) { + return createPatternMatch(PatternMatchKind.Substring, punctuationStripped, + /*isCaseSensitive:*/ partStartsWith(candidate, span, chunk.text, /*ignoreCase:*/ false)); + } + } + } + } + else { + // d) If the part was not entirely lowercase, then check if it is contained in the + // candidate in a case *sensitive* manner. If so, return that there was a substring + // match. + if (candidate.indexOf(chunk.text) > 0) { + return createPatternMatch(PatternMatchKind.Substring, punctuationStripped, /*isCaseSensitive:*/ true); + } + } + + if (!isLowercase) { + // e) If the part was not entirely lowercase, then attempt a camel cased match as well. + if (chunk.characterSpans.length > 0) { + var candidateParts = getWordSpans(candidate); + var camelCaseWeight = tryCamelCaseMatch(candidate, candidateParts, chunk, /*ignoreCase:*/ false); + if (camelCaseWeight !== undefined) { + return createPatternMatch(PatternMatchKind.CamelCase, punctuationStripped, /*isCaseSensitive:*/ true, /*camelCaseWeight:*/ camelCaseWeight); + } + + camelCaseWeight = tryCamelCaseMatch(candidate, candidateParts, chunk, /*ignoreCase:*/ true); + if (camelCaseWeight !== undefined) { + return createPatternMatch(PatternMatchKind.CamelCase, punctuationStripped, /*isCaseSensitive:*/ false, /*camelCaseWeight:*/ camelCaseWeight); + } + } + } + + if (isLowercase) { + // f) Is the pattern a substring of the candidate starting on one of the candidate's word boundaries? + + // We could check every character boundary start of the candidate for the pattern. However, that's + // an m * n operation in the wost case. Instead, find the first instance of the pattern + // substring, and see if it starts on a capital letter. It seems unlikely that the user will try to + // filter the list based on a substring that starts on a capital letter and also with a lowercase one. + // (Pattern: fogbar, Candidate: quuxfogbarFogBar). + if (chunk.text.length < candidate.length) { + if (index > 0 && isUpperCaseLetter(candidate.charCodeAt(index))) { + return createPatternMatch(PatternMatchKind.Substring, punctuationStripped, /*isCaseSensitive:*/ false); + } + } + } + + return undefined; + } + + function containsSpaceOrAsterisk(text: string): boolean { + for (var i = 0; i < text.length; i++) { + var ch = text.charCodeAt(i); + if (ch === CharacterCodes.space || ch === CharacterCodes.asterisk) { + return true; + } + } + + return false; + } + + function matchSegment(candidate: string, segment: Segment): PatternMatch[] { + // First check if the segment matches as is. This is also useful if the segment contains + // characters we would normally strip when splitting into parts that we also may want to + // match in the candidate. For example if the segment is "@int" and the candidate is + // "@int", then that will show up as an exact match here. + // + // Note: if the segment contains a space or an asterisk then we must assume that it's a + // multi-word segment. + if (!containsSpaceOrAsterisk(segment.totalTextChunk.text)) { + var match = matchTextChunk(candidate, segment.totalTextChunk, /*punctuationStripped:*/ false); + if (match) { + return [match]; + } + } + + // The logic for pattern matching is now as follows: + // + // 1) Break the segment passed in into words. Breaking is rather simple and a + // good way to think about it that if gives you all the individual alphanumeric words + // of the pattern. + // + // 2) For each word try to match the word against the candidate value. + // + // 3) Matching is as follows: + // + // a) Check if the word matches the candidate entirely, in an case insensitive or + // sensitive manner. If it does, return that there was an exact match. + // + // b) Check if the word is a prefix of the candidate, in a case insensitive or + // sensitive manner. If it does, return that there was a prefix match. + // + // c) If the word is entirely lowercase, then check if it is contained anywhere in the + // candidate in a case insensitive manner. If so, return that there was a substring + // match. + // + // Note: We only have a substring match if the lowercase part is prefix match of + // some word part. That way we don't match something like 'Class' when the user + // types 'a'. But we would match 'FooAttribute' (since 'Attribute' starts with + // 'a'). + // + // d) If the word was not entirely lowercase, then check if it is contained in the + // candidate in a case *sensitive* manner. If so, return that there was a substring + // match. + // + // e) If the word was not entirely lowercase, then attempt a camel cased match as + // well. + // + // f) The word is all lower case. Is it a case insensitive substring of the candidate starting + // on a part boundary of the candidate? + // + // Only if all words have some sort of match is the pattern considered matched. + + var subWordTextChunks = segment.subWordTextChunks; + var matches: PatternMatch[] = undefined; + + for (var i = 0, n = subWordTextChunks.length; i < n; i++) { + var subWordTextChunk = subWordTextChunks[i]; + + // Try to match the candidate with this word + var result = matchTextChunk(candidate, subWordTextChunk, /*punctuationStripped:*/ true); + if (!result) { + return undefined; + } + + matches = matches || []; + matches.push(result); + } + + return matches; + } + + function partStartsWith(candidate: string, candidateSpan: TextSpan, pattern: string, ignoreCase: boolean, patternSpan?: TextSpan): boolean { + var patternPartStart = patternSpan ? patternSpan.start : 0; + var patternPartLength = patternSpan ? patternSpan.length : pattern.length; + + if (patternPartLength > candidateSpan.length) { + // Pattern part is longer than the candidate part. There can never be a match. + return false; + } + + if (ignoreCase) { + for (var i = 0; i < patternPartLength; i++) { + var ch1 = pattern.charCodeAt(patternPartStart + i); + var ch2 = candidate.charCodeAt(candidateSpan.start + i); + if (toLowerCase(ch1) !== toLowerCase(ch2)) { + return false; + } + } + } + else { + for (var i = 0; i < patternPartLength; i++) { + var ch1 = pattern.charCodeAt(patternPartStart + i); + var ch2 = candidate.charCodeAt(candidateSpan.start + i); + if (ch1 !== ch2) { + return false; + } + } + } + + return true; + } + + function tryCamelCaseMatch(candidate: string, candidateParts: TextSpan[], chunk: TextChunk, ignoreCase: boolean): number { + var chunkCharacterSpans = chunk.characterSpans; + + // Note: we may have more pattern parts than candidate parts. This is because multiple + // pattern parts may match a candidate part. For example "SiUI" against "SimpleUI". + // We'll have 3 pattern parts Si/U/I against two candidate parts Simple/UI. However, U + // and I will both match in UI. + + var currentCandidate = 0; + var currentChunkSpan = 0; + var firstMatch: number = undefined; + var contiguous: boolean = undefined; + + while (true) { + // Let's consider our termination cases + if (currentChunkSpan === chunkCharacterSpans.length) { + // We did match! We shall assign a weight to this + var weight = 0; + + // Was this contiguous? + if (contiguous) { + weight += 1; + } + + // Did we start at the beginning of the candidate? + if (firstMatch === 0) { + weight += 2; + } + + return weight; + } + else if (currentCandidate === candidateParts.length) { + // No match, since we still have more of the pattern to hit + return undefined; + } + + var candidatePart = candidateParts[currentCandidate]; + var gotOneMatchThisCandidate = false; + + // Consider the case of matching SiUI against SimpleUIElement. The candidate parts + // will be Simple/UI/Element, and the pattern parts will be Si/U/I. We'll match 'Si' + // against 'Simple' first. Then we'll match 'U' against 'UI'. However, we want to + // still keep matching pattern parts against that candidate part. + for (; currentChunkSpan < chunkCharacterSpans.length; currentChunkSpan++) { + var chunkCharacterSpan = chunkCharacterSpans[currentChunkSpan]; + + if (gotOneMatchThisCandidate) { + // We've already gotten one pattern part match in this candidate. We will + // only continue trying to consumer pattern parts if the last part and this + // part are both upper case. + if (!isUpperCaseLetter(chunk.text.charCodeAt(chunkCharacterSpans[currentChunkSpan - 1].start)) || + !isUpperCaseLetter(chunk.text.charCodeAt(chunkCharacterSpans[currentChunkSpan].start))) { + break; + } + } + + if (!partStartsWith(candidate, candidatePart, chunk.text, ignoreCase, chunkCharacterSpan)) { + break; + } + + gotOneMatchThisCandidate = true; + + firstMatch = firstMatch === undefined ? currentCandidate : firstMatch; + + // If we were contiguous, then keep that value. If we weren't, then keep that + // value. If we don't know, then set the value to 'true' as an initial match is + // obviously contiguous. + contiguous = contiguous === undefined ? true : contiguous; + + candidatePart = createTextSpan(candidatePart.start + chunkCharacterSpan.length, candidatePart.length - chunkCharacterSpan.length); + } + + // Check if we matched anything at all. If we didn't, then we need to unset the + // contiguous bit if we currently had it set. + // If we haven't set the bit yet, then that means we haven't matched anything so + // far, and we don't want to change that. + if (!gotOneMatchThisCandidate && contiguous !== undefined) { + contiguous = false; + } + + // Move onto the next candidate. + currentCandidate++; + } + } + } + + // Helper function to compare two matches to determine which is better. Matches are first + // ordered by kind (so all prefix matches always beat all substring matches). Then, if the + // match is a camel case match, the relative weights of hte match are used to determine + // which is better (with a greater weight being better). Then if the match is of the same + // type, then a case sensitive match is considered better than an insensitive one. + function patternMatchCompareTo(match1: PatternMatch, match2: PatternMatch): number { + return compareType(match1, match2) || + compareCamelCase(match1, match2) || + compareCase(match1, match2) || + comparePunctuation(match1, match2); + } + + function comparePunctuation(result1: PatternMatch, result2: PatternMatch) { + // Consider a match to be better if it was successful without stripping punctuation + // versus a match that had to strip punctuation to succeed. + if (result1.punctuationStripped !== result2.punctuationStripped) { + return result1.punctuationStripped ? 1 : -1; + } + + return 0; + } + + function compareCase(result1: PatternMatch, result2: PatternMatch) { + if (result1.isCaseSensitive !== result2.isCaseSensitive) { + return result1.isCaseSensitive ? -1 : 1; + } + + return 0; + } + + function compareType(result1: PatternMatch, result2: PatternMatch) { + return result1.kind - result2.kind; + } + + function compareCamelCase(result1: PatternMatch, result2: PatternMatch) { + if (result1.kind === PatternMatchKind.CamelCase && result2.kind === PatternMatchKind.CamelCase) { + // Swap the values here. If result1 has a higher weight, then we want it to come + // first. + return result2.camelCaseWeight - result1.camelCaseWeight; + } + + return 0; + } + + function createSegment(text: string): Segment { + return { + totalTextChunk: createTextChunk(text), + subWordTextChunks: breakPatternIntoTextChunks(text) + } + } + + // A segment is considered invalid if we couldn't find any words in it. + function segmentIsInvalid(segment: Segment) { + return segment.subWordTextChunks.length === 0; + } + + function isUpperCaseLetter(ch: number) { + // Fast check for the ascii range. + if (ch >= CharacterCodes.A && ch <= CharacterCodes.Z) { + return true; + } + + if (ch < CharacterCodes.maxAsciiCharacter || !isUnicodeIdentifierStart(ch, ScriptTarget.Latest)) { + return false; + } + + // TODO: find a way to determine this for any unicode characters in a + // non-allocating manner. + var str = String.fromCharCode(ch); + return str === str.toUpperCase(); + } + + function isLowerCaseLetter(ch: number) { + // Fast check for the ascii range. + if (ch >= CharacterCodes.a && ch <= CharacterCodes.z) { + return true; + } + + if (ch < CharacterCodes.maxAsciiCharacter || !isUnicodeIdentifierStart(ch, ScriptTarget.Latest)) { + return false; + } + + + // TODO: find a way to determine this for any unicode characters in a + // non-allocating manner. + var str = String.fromCharCode(ch); + return str === str.toLowerCase(); + } + + function containsUpperCaseLetter(string: string): boolean { + for (var i = 0, n = string.length; i < n; i++) { + if (isUpperCaseLetter(string.charCodeAt(i))) { + return true; + } + } + + return false; + } + + function startsWith(string: string, search: string) { + for (var i = 0, n = search.length; i < n; i++) { + if (string.charCodeAt(i) !== search.charCodeAt(i)) { + return false; + } + } + + return true; + } + + // Assumes 'value' is already lowercase. + function indexOfIgnoringCase(string: string, value: string): number { + for (var i = 0, n = string.length - value.length; i <= n; i++) { + if (startsWithIgnoringCase(string, value, i)) { + return i; + } + } + + return -1; + } + + // Assumes 'value' is already lowercase. + function startsWithIgnoringCase(string: string, value: string, start: number): boolean { + for (var i = 0, n = value.length; i < n; i++) { + var ch1 = toLowerCase(string.charCodeAt(i + start)); + var ch2 = value.charCodeAt(i); + + if (ch1 !== ch2) { + return false; + } + } + + return true; + } + + function toLowerCase(ch: number): number { + // Fast convert for the ascii range. + if (ch >= CharacterCodes.A && ch <= CharacterCodes.Z) { + return CharacterCodes.a + (ch - CharacterCodes.A); + } + + if (ch < CharacterCodes.maxAsciiCharacter) { + return ch; + } + + // TODO: find a way to compute this for any unicode characters in a + // non-allocating manner. + return String.fromCharCode(ch).toLowerCase().charCodeAt(0); + } + + function isDigit(ch: number) { + // TODO(cyrusn): Find a way to support this for unicode digits. + return ch >= CharacterCodes._0 && ch <= CharacterCodes._9; + } + + function isWordChar(ch: number) { + return isUpperCaseLetter(ch) || isLowerCaseLetter(ch) || isDigit(ch) || ch === CharacterCodes._ || ch === CharacterCodes.$; + } + + function breakPatternIntoTextChunks(pattern: string): TextChunk[] { + var result: TextChunk[] = []; + var wordStart = 0; + var wordLength = 0; + + for (var i = 0; i < pattern.length; i++) { + var ch = pattern.charCodeAt(i); + if (isWordChar(ch)) { + if (wordLength++ === 0) { + wordStart = i; + } + } + else { + if (wordLength > 0) { + result.push(createTextChunk(pattern.substr(wordStart, wordLength))); + wordLength = 0; + } + } + } + + if (wordLength > 0) { + result.push(createTextChunk(pattern.substr(wordStart, wordLength))); + } + + return result; + } + + function createTextChunk(text: string): TextChunk { + var textLowerCase = text.toLowerCase(); + return { + text, + textLowerCase, + isLowerCase: text === textLowerCase, + characterSpans: breakIntoCharacterSpans(text) + } + } + + /* @internal */ export function breakIntoCharacterSpans(identifier: string): TextSpan[] { + return breakIntoSpans(identifier, /*word:*/ false); + } + + /* @internal */ export function breakIntoWordSpans(identifier: string): TextSpan[] { + return breakIntoSpans(identifier, /*word:*/ true); + } + + function breakIntoSpans(identifier: string, word: boolean): TextSpan[] { + var result: TextSpan[] = []; + + var wordStart = 0; + for (var i = 1, n = identifier.length; i < n; i++) { + var lastIsDigit = isDigit(identifier.charCodeAt(i - 1)); + var currentIsDigit = isDigit(identifier.charCodeAt(i)); + + var hasTransitionFromLowerToUpper = transitionFromLowerToUpper(identifier, word, i); + var hasTransitionFromUpperToLower = transitionFromUpperToLower(identifier, word, i, wordStart); + + if (charIsPunctuation(identifier.charCodeAt(i - 1)) || + charIsPunctuation(identifier.charCodeAt(i)) || + lastIsDigit != currentIsDigit || + hasTransitionFromLowerToUpper || + hasTransitionFromUpperToLower) { + + if (!isAllPunctuation(identifier, wordStart, i)) { + result.push(createTextSpan(wordStart, i - wordStart)); + } + + wordStart = i; + } + } + + if (!isAllPunctuation(identifier, wordStart, identifier.length)) { + result.push(createTextSpan(wordStart, identifier.length - wordStart)); + } + + return result; + } + + function charIsPunctuation(ch: number) { + switch (ch) { + case CharacterCodes.exclamation: + case CharacterCodes.doubleQuote: + case CharacterCodes.hash: + case CharacterCodes.percent: + case CharacterCodes.ampersand: + case CharacterCodes.singleQuote: + case CharacterCodes.openParen: + case CharacterCodes.closeParen: + case CharacterCodes.asterisk: + case CharacterCodes.comma: + case CharacterCodes.minus: + case CharacterCodes.dot: + case CharacterCodes.slash: + case CharacterCodes.colon: + case CharacterCodes.semicolon: + case CharacterCodes.question: + case CharacterCodes.at: + case CharacterCodes.openBracket: + case CharacterCodes.backslash: + case CharacterCodes.closeBracket: + case CharacterCodes._: + case CharacterCodes.openBrace: + case CharacterCodes.closeBrace: + return true; + } + + return false; + } + + function isAllPunctuation(identifier: string, start: number, end: number): boolean { + for (var i = start; i < end; i++) { + var ch = identifier.charCodeAt(i); + + // We don't consider _ or $ as punctuation as there may be things with that name. + if (!charIsPunctuation(ch) || ch === CharacterCodes._ || ch === CharacterCodes.$) { + return false; + } + } + + return true; + } + + function transitionFromUpperToLower(identifier: string, word: boolean, index: number, wordStart: number): boolean { + if (word) { + // Cases this supports: + // 1) IDisposable -> I, Disposable + // 2) UIElement -> UI, Element + // 3) HTMLDocument -> HTML, Document + // + // etc. + if (index != wordStart && + index + 1 < identifier.length) { + var currentIsUpper = isUpperCaseLetter(identifier.charCodeAt(index)); + var nextIsLower = isLowerCaseLetter(identifier.charCodeAt(index + 1)); + + if (currentIsUpper && nextIsLower) { + // We have a transition from an upper to a lower letter here. But we only + // want to break if all the letters that preceded are uppercase. i.e. if we + // have "Foo" we don't want to break that into "F, oo". But if we have + // "IFoo" or "UIFoo", then we want to break that into "I, Foo" and "UI, + // Foo". i.e. the last uppercase letter belongs to the lowercase letters + // that follows. Note: this will make the following not split properly: + // "HELLOthere". However, these sorts of names do not show up in .Net + // programs. + for (var i = wordStart; i < index; i++) { + if (!isUpperCaseLetter(identifier.charCodeAt(i))) { + return false; + } + } + + return true; + } + } + } + + return false; + } + + function transitionFromLowerToUpper(identifier: string, word: boolean, index: number): boolean { + var lastIsUpper = isUpperCaseLetter(identifier.charCodeAt(index - 1)); + var currentIsUpper = isUpperCaseLetter(identifier.charCodeAt(index)); + + // See if the casing indicates we're starting a new word. Note: if we're breaking on + // words, then just seeing an upper case character isn't enough. Instead, it has to + // be uppercase and the previous character can't be uppercase. + // + // For example, breaking "AddMetadata" on words would make: Add Metadata + // + // on characters would be: A dd M etadata + // + // Break "AM" on words would be: AM + // + // on characters would be: A M + // + // We break the search string on characters. But we break the symbol name on words. + var transition = word + ? (currentIsUpper && !lastIsUpper) + : currentIsUpper; + return transition; + } +} \ No newline at end of file diff --git a/src/services/services.ts b/src/services/services.ts index f7b70794eef..5099fb05c7b 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -4,13 +4,13 @@ /// /// /// +/// /// /// /// /// module ts { - export var servicesVersion = "0.4" export interface Node { diff --git a/tests/baselines/reference/APISample_compile.js b/tests/baselines/reference/APISample_compile.js index 85bf517e3e1..edc4ee297aa 100644 --- a/tests/baselines/reference/APISample_compile.js +++ b/tests/baselines/reference/APISample_compile.js @@ -1326,6 +1326,7 @@ declare module "typescript" { equals = 61, exclamation = 33, greaterThan = 62, + hash = 35, lessThan = 60, minus = 45, openBrace = 123, diff --git a/tests/baselines/reference/APISample_compile.types b/tests/baselines/reference/APISample_compile.types index d5c45de71b5..9a31d48980b 100644 --- a/tests/baselines/reference/APISample_compile.types +++ b/tests/baselines/reference/APISample_compile.types @@ -4190,6 +4190,9 @@ declare module "typescript" { greaterThan = 62, >greaterThan : CharacterCodes + hash = 35, +>hash : CharacterCodes + lessThan = 60, >lessThan : CharacterCodes diff --git a/tests/baselines/reference/APISample_linter.js b/tests/baselines/reference/APISample_linter.js index 29bc4802f23..50ed483b4a3 100644 --- a/tests/baselines/reference/APISample_linter.js +++ b/tests/baselines/reference/APISample_linter.js @@ -1357,6 +1357,7 @@ declare module "typescript" { equals = 61, exclamation = 33, greaterThan = 62, + hash = 35, lessThan = 60, minus = 45, openBrace = 123, diff --git a/tests/baselines/reference/APISample_linter.types b/tests/baselines/reference/APISample_linter.types index fc1389496ba..512cd689303 100644 --- a/tests/baselines/reference/APISample_linter.types +++ b/tests/baselines/reference/APISample_linter.types @@ -4336,6 +4336,9 @@ declare module "typescript" { greaterThan = 62, >greaterThan : CharacterCodes + hash = 35, +>hash : CharacterCodes + lessThan = 60, >lessThan : CharacterCodes diff --git a/tests/baselines/reference/APISample_transform.js b/tests/baselines/reference/APISample_transform.js index 35927ec43f3..8b3c64099cc 100644 --- a/tests/baselines/reference/APISample_transform.js +++ b/tests/baselines/reference/APISample_transform.js @@ -1358,6 +1358,7 @@ declare module "typescript" { equals = 61, exclamation = 33, greaterThan = 62, + hash = 35, lessThan = 60, minus = 45, openBrace = 123, diff --git a/tests/baselines/reference/APISample_transform.types b/tests/baselines/reference/APISample_transform.types index b3fa5af4c20..83ef90f7268 100644 --- a/tests/baselines/reference/APISample_transform.types +++ b/tests/baselines/reference/APISample_transform.types @@ -4286,6 +4286,9 @@ declare module "typescript" { greaterThan = 62, >greaterThan : CharacterCodes + hash = 35, +>hash : CharacterCodes + lessThan = 60, >lessThan : CharacterCodes diff --git a/tests/baselines/reference/APISample_watcher.js b/tests/baselines/reference/APISample_watcher.js index ada6a599651..d9488db8bc1 100644 --- a/tests/baselines/reference/APISample_watcher.js +++ b/tests/baselines/reference/APISample_watcher.js @@ -1395,6 +1395,7 @@ declare module "typescript" { equals = 61, exclamation = 33, greaterThan = 62, + hash = 35, lessThan = 60, minus = 45, openBrace = 123, diff --git a/tests/baselines/reference/APISample_watcher.types b/tests/baselines/reference/APISample_watcher.types index 75c3c0130d2..11a726ea39f 100644 --- a/tests/baselines/reference/APISample_watcher.types +++ b/tests/baselines/reference/APISample_watcher.types @@ -4459,6 +4459,9 @@ declare module "typescript" { greaterThan = 62, >greaterThan : CharacterCodes + hash = 35, +>hash : CharacterCodes + lessThan = 60, >lessThan : CharacterCodes diff --git a/tests/cases/unittests/services/patternMatcher.ts b/tests/cases/unittests/services/patternMatcher.ts new file mode 100644 index 00000000000..ddf1f9fe344 --- /dev/null +++ b/tests/cases/unittests/services/patternMatcher.ts @@ -0,0 +1,532 @@ +/// +/// + +describe('PatternMatcher', function () { + describe("BreakIntoCharacterSpans", function () { + it("EmptyIdentifier", () => { + verifyBreakIntoCharacterSpans("") + }); + + it("SimpleIdentifier", () => { + verifyBreakIntoCharacterSpans("foo", "foo"); + }); + + it("PrefixUnderscoredIdentifier", () => { + verifyBreakIntoCharacterSpans("_foo", "_", "foo"); + }); + + it("UnderscoredIdentifier", () => { + verifyBreakIntoCharacterSpans("f_oo", "f", "_", "oo"); + }); + + it("PostfixUnderscoredIdentifier", () => { + verifyBreakIntoCharacterSpans("foo_", "foo", "_"); + }); + + it("PrefixUnderscoredIdentifierWithCapital", () => { + verifyBreakIntoCharacterSpans("_Foo", "_", "Foo"); + }); + + it("MUnderscorePrefixed", () => { + verifyBreakIntoCharacterSpans("m_foo", "m", "_", "foo"); + }); + + it("CamelCaseIdentifier", () => { + verifyBreakIntoCharacterSpans("FogBar", "Fog", "Bar"); + }); + + it("MixedCaseIdentifier", () => { + verifyBreakIntoCharacterSpans("fogBar", "fog", "Bar"); + }); + + it("TwoCharacterCapitalIdentifier", () => { + verifyBreakIntoCharacterSpans("UIElement", "U", "I", "Element"); + }); + + it("NumberSuffixedIdentifier", () => { + verifyBreakIntoCharacterSpans("Foo42", "Foo", "42"); + }); + + it("NumberContainingIdentifier", () => { + verifyBreakIntoCharacterSpans("Fog42Bar", "Fog", "42", "Bar"); + }); + + it("NumberPrefixedIdentifier", () => { + verifyBreakIntoCharacterSpans("42Bar", "42", "Bar"); + }); + }); + + describe("BreakIntoWordSpans", function () { + it("VarbatimIdentifier", () => { + verifyBreakIntoWordSpans("@int:", "int"); + }); + + it("AllCapsConstant", () => { + verifyBreakIntoWordSpans("C_STYLE_CONSTANT", "C", "_", "STYLE", "_", "CONSTANT"); + }); + + it("SingleLetterPrefix1", () => { + verifyBreakIntoWordSpans("UInteger", "U", "Integer"); + }); + + it("SingleLetterPrefix2", () => { + verifyBreakIntoWordSpans("IDisposable", "I", "Disposable"); + }); + + it("TwoCharacterCapitalIdentifier", () => { + verifyBreakIntoWordSpans("UIElement", "UI", "Element"); + }); + + it("XDocument", () => { + verifyBreakIntoWordSpans("XDocument", "X", "Document"); + }); + + it("XMLDocument1", () => { + verifyBreakIntoWordSpans("XMLDocument", "XML", "Document"); + }); + + it("XMLDocument2", () => { + verifyBreakIntoWordSpans("XmlDocument", "Xml", "Document"); + }); + + it("TwoUppercaseCharacters", () => { + verifyBreakIntoWordSpans("SimpleUIElement", "Simple", "UI", "Element"); + }); + }); + + describe("SingleWordPattern", () => { + it("PreferCaseSensitiveExact", () => { + debugger; + var match = getFirstMatch("Foo", "Foo"); + + assert.equal(ts.PatternMatchKind.Exact, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveExactInsensitive", () => { + var match = getFirstMatch("foo", "Foo"); + + assert.equal(ts.PatternMatchKind.Exact, match.kind); + assert.equal(false, match.isCaseSensitive); + }); + + it("PreferCaseSensitivePrefix", () => { + var match = getFirstMatch("Foo", "Fo"); + + assert.equal(ts.PatternMatchKind.Prefix, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("PreferCaseSensitivePrefixCaseInsensitive", () => { + var match = getFirstMatch("Foo", "fo"); + + assert.equal(ts.PatternMatchKind.Prefix, match.kind); + assert.equal(false, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveCamelCaseMatchSimple", () => { + debugger; + var match = getFirstMatch("FogBar", "FB"); + + assert.equal(ts.PatternMatchKind.CamelCase, match.kind); + assert.equal(true, match.isCaseSensitive); + assertInRange(match.camelCaseWeight, 1, 1 << 30); + }); + + it("PreferCaseSensitiveCamelCaseMatchPartialPattern", () => { + var match = getFirstMatch("FogBar", "FoB"); + + assert.equal(ts.PatternMatchKind.CamelCase, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveCamelCaseMatchToLongPattern1", () => { + var match = getFirstMatch("FogBar", "FBB"); + + assert.isTrue(match === undefined); + }); + + it("PreferCaseSensitiveCamelCaseMatchToLongPattern2", () => { + var match = getFirstMatch("FogBar", "FoooB"); + + assert.isTrue(match === undefined); + }); + + it("CamelCaseMatchPartiallyUnmatched", () => { + var match = getFirstMatch("FogBarBaz", "FZ"); + + assert.isTrue(match === undefined); + }); + + it("CamelCaseMatchCompletelyUnmatched", () => { + var match = getFirstMatch("FogBarBaz", "ZZ"); + + assert.isTrue(match === undefined); + }); + + it("TwoUppercaseCharacters", () => { + var match = getFirstMatch("SimpleUIElement", "SiUI"); + + assert.equal(ts.PatternMatchKind.CamelCase, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveLowercasePattern", () => { + debugger; + var match = getFirstMatch("FogBar", "b"); + + assert.equal(ts.PatternMatchKind.Substring, match.kind); + assert.equal(false, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveLowercasePattern2", () => { + var match = getFirstMatch("FogBar", "fB"); + + assert.equal(ts.PatternMatchKind.CamelCase, match.kind); + assert.equal(false, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveTryUnderscoredName", () => { + var match = getFirstMatch("_fogBar", "_fB"); + + assert.equal(ts.PatternMatchKind.CamelCase, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveTryUnderscoredName2", () => { + var match = getFirstMatch("_fogBar", "fB"); + + assert.equal(ts.PatternMatchKind.CamelCase, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveTryUnderscoredNameInsensitive", () => { + var match = getFirstMatch("_FogBar", "_fB"); + + assert.equal(ts.PatternMatchKind.CamelCase, match.kind); + assert.equal(false, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveMiddleUnderscore", () => { + var match = getFirstMatch("Fog_Bar", "FB"); + + assert.equal(ts.PatternMatchKind.CamelCase, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveMiddleUnderscore2", () => { + var match = getFirstMatch("Fog_Bar", "F_B"); + + assert.equal(ts.PatternMatchKind.CamelCase, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveMiddleUnderscore3", () => { + var match = getFirstMatch("Fog_Bar", "F__B"); + + assert.isTrue(undefined === match); + }); + + it("PreferCaseSensitiveMiddleUnderscore4", () => { + var match = getFirstMatch("Fog_Bar", "f_B"); + + assert.equal(ts.PatternMatchKind.CamelCase, match.kind); + assert.equal(false, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveMiddleUnderscore5", () => { + var match = getFirstMatch("Fog_Bar", "F_b"); + + assert.equal(ts.PatternMatchKind.CamelCase, match.kind); + assert.equal(false, match.isCaseSensitive); + }); + + it("PreferCaseSensitiveRelativeWeights1", () => { + var match1 = getFirstMatch("FogBarBaz", "FB"); + var match2 = getFirstMatch("FooFlobBaz", "FB"); + + // We should prefer something that starts at the beginning if possible + assertInRange(match1.camelCaseWeight, match2.camelCaseWeight + 1, 1 << 30); + }); + + it("PreferCaseSensitiveRelativeWeights2", () => { + var match1 = getFirstMatch("BazBarFooFooFoo", "FFF"); + var match2 = getFirstMatch("BazFogBarFooFoo", "FFF"); + + // Contiguous things should also be preferred + assertInRange(match1.camelCaseWeight, match2.camelCaseWeight + 1, 1 << 30); + }); + + it("PreferCaseSensitiveRelativeWeights3", () => { + var match1 = getFirstMatch("FogBarFooFoo", "FFF"); + var match2 = getFirstMatch("BarFooFooFoo", "FFF"); + + // The weight of being first should be greater than the weight of being contiguous + assertInRange(match1.camelCaseWeight, match2.camelCaseWeight + 1, 1 << 30); + }); + + it("AllLowerPattern1", () => { + debugger; + var match = getFirstMatch("FogBarChangedEventArgs", "changedeventargs"); + + assert.isTrue(undefined !== match); + }); + + it("AllLowerPattern2", () => { + var match = getFirstMatch("FogBarChangedEventArgs", "changedeventarrrgh"); + + assert.isTrue(undefined === match); + }); + + it("AllLowerPattern3", () => { + var match = getFirstMatch("ABCDEFGH", "bcd"); + + assert.isTrue(undefined !== match); + }); + + it("AllLowerPattern4", () => { + var match = getFirstMatch("AbcdefghijEfgHij", "efghij"); + + assert.isTrue(undefined === match); + }); + }); + + describe("MultiWordPattern", () => { + it("ExactWithLowercase", () => { + var matches = getAllMatches("AddMetadataReference", "addmetadatareference"); + + assertContainsKind(ts.PatternMatchKind.Exact, matches); + }); + + it("SingleLowercasedSearchWord1", () => { + var matches = getAllMatches("AddMetadataReference", "add"); + + assertContainsKind(ts.PatternMatchKind.Prefix, matches); + }); + + it("SingleLowercasedSearchWord2", () => { + var matches = getAllMatches("AddMetadataReference", "metadata"); + + assertContainsKind(ts.PatternMatchKind.Substring, matches); + }); + + it("SingleUppercaseSearchWord1", () => { + var matches = getAllMatches("AddMetadataReference", "Add"); + + assertContainsKind(ts.PatternMatchKind.Prefix, matches); + }); + + it("SingleUppercaseSearchWord2", () => { + var matches = getAllMatches("AddMetadataReference", "Metadata"); + + assertContainsKind(ts.PatternMatchKind.Substring, matches); + }); + + it("SingleUppercaseSearchLetter1", () => { + var matches = getAllMatches("AddMetadataReference", "A"); + + assertContainsKind(ts.PatternMatchKind.Prefix, matches); + }); + + it("SingleUppercaseSearchLetter2", () => { + var matches = getAllMatches("AddMetadataReference", "M"); + + assertContainsKind(ts.PatternMatchKind.Substring, matches); + }); + + it("TwoLowercaseWords", () => { + var matches = getAllMatches("AddMetadataReference", "add metadata"); + + assertContainsKind(ts.PatternMatchKind.Prefix, matches); + assertContainsKind(ts.PatternMatchKind.Substring, matches); + }); + + it("TwoLowercaseWords", () => { + var matches = getAllMatches("AddMetadataReference", "A M"); + + assertContainsKind(ts.PatternMatchKind.Prefix, matches); + assertContainsKind(ts.PatternMatchKind.Substring, matches); + }); + + it("TwoLowercaseWords", () => { + var matches = getAllMatches("AddMetadataReference", "AM"); + + assertContainsKind(ts.PatternMatchKind.CamelCase, matches); + }); + + it("TwoLowercaseWords", () => { + var matches = getAllMatches("AddMetadataReference", "ref Metadata") + + assertArrayEquals(ts.map(matches, m => m.kind), [ts.PatternMatchKind.Substring, ts.PatternMatchKind.Substring]); + }); + + it("TwoLowercaseWords", () => { + var matches = getAllMatches("AddMetadataReference", "ref M") + + assertArrayEquals(ts.map(matches, m => m.kind), [ts.PatternMatchKind.Substring, ts.PatternMatchKind.Substring]); + }); + + it("MixedCamelCase", () => { + var matches = getAllMatches("AddMetadataReference", "AMRe"); + + assertContainsKind(ts.PatternMatchKind.CamelCase, matches); + }); + + it("BlankPattern", () => { + var matches = getAllMatches("AddMetadataReference", ""); + + assert.isTrue(matches === undefined); + }); + + it("WhitespaceOnlyPattern", () => { + var matches = getAllMatches("AddMetadataReference", " "); + + assert.isTrue(matches === undefined); + }); + + it("EachWordSeparately1", () => { + var matches = getAllMatches("AddMetadataReference", "add Meta"); + + assertContainsKind(ts.PatternMatchKind.Prefix, matches); + assertContainsKind(ts.PatternMatchKind.Substring, matches); + }); + + it("EachWordSeparately2", () => { + var matches = getAllMatches("AddMetadataReference", "Add meta"); + + assertContainsKind(ts.PatternMatchKind.Prefix, matches); + assertContainsKind(ts.PatternMatchKind.Substring, matches); + }); + + it("EachWordSeparately3", () => { + var matches = getAllMatches("AddMetadataReference", "Add Meta"); + + assertContainsKind(ts.PatternMatchKind.Prefix, matches); + assertContainsKind(ts.PatternMatchKind.Substring, matches); + }); + + it("MixedCasing", () => { + var matches = getAllMatches("AddMetadataReference", "mEta"); + + assert.isTrue(matches === undefined); + }); + + it("MixedCasing2", () => { + var matches = getAllMatches("AddMetadataReference", "Data"); + + assert.isTrue(matches === undefined); + }); + + it("AsteriskSplit", () => { + var matches = getAllMatches("GetKeyWord", "K*W"); + + assertArrayEquals(ts.map(matches, m => m.kind), [ts.PatternMatchKind.Substring, ts.PatternMatchKind.Substring]); + }); + + it("LowercaseSubstring1", () => { + var matches = getAllMatches("Operator", "a"); + + assert.isTrue(matches === undefined); + }); + + it("LowercaseSubstring2", () => { + var matches = getAllMatches("FooAttribute", "a"); + assertContainsKind(ts.PatternMatchKind.Substring, matches); + assert.isFalse(matches[0].isCaseSensitive); + }); + }); + + describe("DottedPattern", () => { + it("DottedPattern1", () => { + var match = getFirstMatchForDottedPattern("Foo.Bar.Baz", "Quux", "B.Q"); + + assert.equal(ts.PatternMatchKind.Prefix, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("DottedPattern2", () => { + var match = getFirstMatchForDottedPattern("Foo.Bar.Baz", "Quux", "C.Q"); + assert.isTrue(match === undefined); + }); + + it("DottedPattern3", () => { + var match = getFirstMatchForDottedPattern("Foo.Bar.Baz", "Quux", "B.B.Q"); + assert.equal(ts.PatternMatchKind.Prefix, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("DottedPattern4", () => { + var match = getFirstMatchForDottedPattern("Foo.Bar.Baz", "Quux", "Baz.Quux"); + assert.equal(ts.PatternMatchKind.Exact, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("DottedPattern5", () => { + var match = getFirstMatchForDottedPattern("Foo.Bar.Baz", "Quux", "F.B.B.Quux"); + assert.equal(ts.PatternMatchKind.Exact, match.kind); + assert.equal(true, match.isCaseSensitive); + }); + + it("DottedPattern6", () => { + var match = getFirstMatchForDottedPattern("Foo.Bar.Baz", "Quux", "F.F.B.B.Quux"); + assert.isTrue(match === undefined); + }); + + it("DottedPattern7", () => { + debugger; + var match = getFirstMatch("UIElement", "UIElement"); + var match = getFirstMatch("GetKeyword", "UIElement"); + assert.isTrue(match === undefined); + }); + }); + + function getFirstMatch(candidate: string, pattern: string): ts.PatternMatch { + var matches = ts.createPatternMatcher(pattern).getMatchesForLastSegmentOfPattern(candidate); + return matches ? matches[0] : undefined; + } + + function getAllMatches(candidate: string, pattern: string): ts.PatternMatch[] { + return ts.createPatternMatcher(pattern).getMatchesForLastSegmentOfPattern(candidate); + } + + function getFirstMatchForDottedPattern(dottedContainer: string, candidate: string, pattern: string): ts.PatternMatch { + var matches = ts.createPatternMatcher(pattern).getMatches(candidate, dottedContainer); + return matches ? matches[0] : undefined; + } + + function spanListToSubstrings(identifier: string, spans: ts.TextSpan[]) { + return ts.map(spans, s => identifier.substr(s.start, s.length)); + } + + function breakIntoCharacterSpans(identifier: string) { + return spanListToSubstrings(identifier, ts.breakIntoCharacterSpans(identifier)); + } + + function breakIntoWordSpans(identifier: string) { + return spanListToSubstrings(identifier, ts.breakIntoWordSpans(identifier)); + } + function assertArrayEquals(array1: T[], array2: T[]) { + assert.equal(array1.length, array2.length); + + for (var i = 0, n = array1.length; i < n; i++) { + assert.equal(array1[i], array2[i]); + } + } + + function assertInRange(val: number, low: number, high: number) { + assert.isTrue(val >= low); + assert.isTrue(val <= high); + } + + function verifyBreakIntoCharacterSpans(original: string, ...parts: string[]): void { + assertArrayEquals(parts, breakIntoCharacterSpans(original)); + } + + function verifyBreakIntoWordSpans(original: string, ...parts: string[]): void { + assertArrayEquals(parts, breakIntoWordSpans(original)); + } + + function assertContainsKind(kind: ts.PatternMatchKind, results: ts.PatternMatch[]) { + assert.isTrue(ts.forEach(results, r => r.kind === kind)); + } +}); \ No newline at end of file