Merge branch 'master' into multiLineEmit2
This commit is contained in:
commit
740f7bb4bf
14 changed files with 1367 additions and 3 deletions
4
Jakefile
4
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([
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1646,6 +1646,7 @@ module ts {
|
|||
equals = 0x3D, // =
|
||||
exclamation = 0x21, // !
|
||||
greaterThan = 0x3E, // >
|
||||
hash = 0x23, // #
|
||||
lessThan = 0x3C, // <
|
||||
minus = 0x2D, // -
|
||||
openBrace = 0x7B, // {
|
||||
|
|
813
src/services/patternMatcher.ts
Normal file
813
src/services/patternMatcher.ts
Normal file
|
@ -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<TextSpan[]> = {};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -4,13 +4,13 @@
|
|||
/// <reference path='outliningElementsCollector.ts' />
|
||||
/// <reference path='navigateTo.ts' />
|
||||
/// <reference path='navigationBar.ts' />
|
||||
/// <reference path='patternMatcher.ts' />
|
||||
/// <reference path='signatureHelp.ts' />
|
||||
/// <reference path='utilities.ts' />
|
||||
/// <reference path='formatting\formatting.ts' />
|
||||
/// <reference path='formatting\smartIndenter.ts' />
|
||||
|
||||
module ts {
|
||||
|
||||
export var servicesVersion = "0.4"
|
||||
|
||||
export interface Node {
|
||||
|
|
|
@ -1326,6 +1326,7 @@ declare module "typescript" {
|
|||
equals = 61,
|
||||
exclamation = 33,
|
||||
greaterThan = 62,
|
||||
hash = 35,
|
||||
lessThan = 60,
|
||||
minus = 45,
|
||||
openBrace = 123,
|
||||
|
|
|
@ -4190,6 +4190,9 @@ declare module "typescript" {
|
|||
greaterThan = 62,
|
||||
>greaterThan : CharacterCodes
|
||||
|
||||
hash = 35,
|
||||
>hash : CharacterCodes
|
||||
|
||||
lessThan = 60,
|
||||
>lessThan : CharacterCodes
|
||||
|
||||
|
|
|
@ -1357,6 +1357,7 @@ declare module "typescript" {
|
|||
equals = 61,
|
||||
exclamation = 33,
|
||||
greaterThan = 62,
|
||||
hash = 35,
|
||||
lessThan = 60,
|
||||
minus = 45,
|
||||
openBrace = 123,
|
||||
|
|
|
@ -4336,6 +4336,9 @@ declare module "typescript" {
|
|||
greaterThan = 62,
|
||||
>greaterThan : CharacterCodes
|
||||
|
||||
hash = 35,
|
||||
>hash : CharacterCodes
|
||||
|
||||
lessThan = 60,
|
||||
>lessThan : CharacterCodes
|
||||
|
||||
|
|
|
@ -1358,6 +1358,7 @@ declare module "typescript" {
|
|||
equals = 61,
|
||||
exclamation = 33,
|
||||
greaterThan = 62,
|
||||
hash = 35,
|
||||
lessThan = 60,
|
||||
minus = 45,
|
||||
openBrace = 123,
|
||||
|
|
|
@ -4286,6 +4286,9 @@ declare module "typescript" {
|
|||
greaterThan = 62,
|
||||
>greaterThan : CharacterCodes
|
||||
|
||||
hash = 35,
|
||||
>hash : CharacterCodes
|
||||
|
||||
lessThan = 60,
|
||||
>lessThan : CharacterCodes
|
||||
|
||||
|
|
|
@ -1395,6 +1395,7 @@ declare module "typescript" {
|
|||
equals = 61,
|
||||
exclamation = 33,
|
||||
greaterThan = 62,
|
||||
hash = 35,
|
||||
lessThan = 60,
|
||||
minus = 45,
|
||||
openBrace = 123,
|
||||
|
|
|
@ -4459,6 +4459,9 @@ declare module "typescript" {
|
|||
greaterThan = 62,
|
||||
>greaterThan : CharacterCodes
|
||||
|
||||
hash = 35,
|
||||
>hash : CharacterCodes
|
||||
|
||||
lessThan = 60,
|
||||
>lessThan : CharacterCodes
|
||||
|
||||
|
|
532
tests/cases/unittests/services/patternMatcher.ts
Normal file
532
tests/cases/unittests/services/patternMatcher.ts
Normal file
|
@ -0,0 +1,532 @@
|
|||
/// <reference path="..\..\..\..\src\harness\external\mocha.d.ts" />
|
||||
/// <reference path="..\..\..\..\src\services\patternMatcher.ts" />
|
||||
|
||||
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<T>(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));
|
||||
}
|
||||
});
|
Loading…
Reference in a new issue