-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Move NavigateTo over to using the new pattern matcher. #2111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,35 +1,46 @@ | ||
module ts.NavigateTo { | ||
type RawNavigateToItem = { name: string; fileName: string; matchKind: MatchKind; declaration: Declaration }; | ||
|
||
enum MatchKind { | ||
none = 0, | ||
exact = 1, | ||
substring = 2, | ||
prefix = 3 | ||
} | ||
|
||
export function getNavigateToItems(program: Program, cancellationToken: CancellationTokenObject, searchValue: string, maxResultCount: number): NavigateToItem[]{ | ||
// Split search value in terms array | ||
var terms = searchValue.split(" "); | ||
|
||
// default NavigateTo approach: if search term contains only lower-case chars - use case-insensitive search, otherwise switch to case-sensitive version | ||
var searchTerms = map(terms, t => ({ caseSensitive: hasAnyUpperCaseCharacter(t), term: t })); | ||
type RawNavigateToItem = { name: string; fileName: string; matchKind: PatternMatchKind; isCaseSensitive: boolean; declaration: Declaration }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What makes it raw? |
||
|
||
export function getNavigateToItems(program: Program, cancellationToken: CancellationTokenObject, searchValue: string, maxResultCount: number): NavigateToItem[] { | ||
var patternMatcher = createPatternMatcher(searchValue); | ||
var rawItems: RawNavigateToItem[] = []; | ||
|
||
// Search the declarations in all files and output matched NavigateToItem into array of NavigateToItem[] | ||
forEach(program.getSourceFiles(), sourceFile => { | ||
cancellationToken.throwIfCancellationRequested(); | ||
|
||
var fileName = sourceFile.fileName; | ||
var declarations = sourceFile.getNamedDeclarations(); | ||
for (var i = 0, n = declarations.length; i < n; i++) { | ||
var declaration = declarations[i]; | ||
// TODO(jfreeman): Skip this declaration if it has a computed name | ||
var name = (<Identifier>declaration.name).text; | ||
var matchKind = getMatchKind(searchTerms, name); | ||
if (matchKind !== MatchKind.none) { | ||
rawItems.push({ name, fileName, matchKind, declaration }); | ||
var name = getDeclarationName(declaration); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will crash on a computed property that is not a well known symbol. Either check for that first, or remove the assert inside. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, I'm thinking of the wrong getDeclarationName, nvm |
||
if (name !== undefined) { | ||
|
||
// First do a quick check to see if the name of the declaration matches the | ||
// last portion of the (possibly) dotted name they're searching for. | ||
var matches = patternMatcher.getMatchesForLastSegmentOfPattern(name); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't getMatches in the pattern matcher already do this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As per discussion, the pattern matcher is concerned only with the domain of strings. It can't know the candidate container unless it is given it. And we don't want to compute the container if we can avoid it. |
||
|
||
if (!matches) { | ||
continue; | ||
} | ||
|
||
// It was a match! If the pattern has dots in it, then also see if hte | ||
// declaration container matches as well. | ||
if (patternMatcher.patternContainsDots) { | ||
var containers = getContainers(declaration); | ||
if (!containers) { | ||
return undefined; | ||
} | ||
|
||
matches = patternMatcher.getMatches(containers, name); | ||
|
||
if (!matches) { | ||
continue; | ||
} | ||
} | ||
|
||
var fileName = sourceFile.fileName; | ||
var matchKind = bestMatchKind(matches); | ||
rawItems.push({ name, fileName, matchKind, isCaseSensitive: allMatchesAreCaseSensitive(matches), declaration }); | ||
} | ||
} | ||
}); | ||
|
@@ -43,6 +54,129 @@ module ts.NavigateTo { | |
|
||
return items; | ||
|
||
function allMatchesAreCaseSensitive(matches: PatternMatch[]): boolean { | ||
Debug.assert(matches.length > 0); | ||
|
||
// This is a case sensitive match, only if all the submatches were case sensitive. | ||
for (var i = 0, n = matches.length; i < n; i++) { | ||
if (!matches[i].isCaseSensitive) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
function getDeclarationName(declaration: Declaration): string { | ||
var result = getTextOfIdentifierOrLiteral(declaration.name); | ||
if (result !== undefined) { | ||
return result; | ||
} | ||
|
||
if (declaration.name.kind === SyntaxKind.ComputedPropertyName) { | ||
var expr = (<ComputedPropertyName>declaration.name).expression; | ||
if (expr.kind === SyntaxKind.PropertyAccessExpression) { | ||
return (<PropertyAccessExpression>expr).name.text; | ||
} | ||
|
||
return getTextOfIdentifierOrLiteral(expr); | ||
} | ||
|
||
return undefined; | ||
} | ||
|
||
function getTextOfIdentifierOrLiteral(node: Node) { | ||
if (node.kind === SyntaxKind.Identifier || | ||
node.kind === SyntaxKind.StringLiteral || | ||
node.kind === SyntaxKind.NumericLiteral) { | ||
|
||
return (<Identifier | LiteralExpression>node).text; | ||
} | ||
|
||
return undefined; | ||
} | ||
|
||
function tryAddSingleDeclarationName(declaration: Declaration, containers: string[]) { | ||
if (declaration && declaration.name) { | ||
var text = getTextOfIdentifierOrLiteral(declaration.name); | ||
if (text !== undefined) { | ||
containers.unshift(text); | ||
} | ||
else if (declaration.name.kind === SyntaxKind.ComputedPropertyName) { | ||
return tryAddComputedPropertyName((<ComputedPropertyName>declaration.name).expression, containers, /*includeLastPortion:*/ true); | ||
} | ||
else { | ||
// Don't know how to add this. | ||
return false | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
// Only added the names of computed properties if they're simple dotted expressions, like: | ||
// | ||
// [X.Y.Z]() { } | ||
function tryAddComputedPropertyName(expression: Expression, containers: string[], includeLastPortion: boolean): boolean { | ||
var text = getTextOfIdentifierOrLiteral(expression); | ||
if (text !== undefined) { | ||
if (includeLastPortion) { | ||
containers.unshift(text); | ||
} | ||
return true; | ||
} | ||
|
||
if (expression.kind === SyntaxKind.PropertyAccessExpression) { | ||
var propertyAccess = <PropertyAccessExpression>expression; | ||
if (includeLastPortion) { | ||
containers.unshift(propertyAccess.name.text); | ||
} | ||
|
||
return tryAddComputedPropertyName(propertyAccess.expression, containers, /*includeLastPortion:*/ true); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
function getContainers(declaration: Declaration) { | ||
var containers: string[] = []; | ||
|
||
// First, if we started with a computed property name, then add all but the last | ||
// portion into the container array. | ||
if (declaration.name.kind === SyntaxKind.ComputedPropertyName) { | ||
if (!tryAddComputedPropertyName((<ComputedPropertyName>declaration.name).expression, containers, /*includeLastPortion:*/ false)) { | ||
return undefined; | ||
} | ||
} | ||
|
||
// Now, walk up our containers, adding all their names to the container array. | ||
declaration = getContainerNode(declaration); | ||
|
||
while (declaration) { | ||
if (!tryAddSingleDeclarationName(declaration, containers)) { | ||
return undefined; | ||
} | ||
|
||
declaration = getContainerNode(declaration); | ||
} | ||
|
||
return containers; | ||
} | ||
|
||
function bestMatchKind(matches: PatternMatch[]) { | ||
Debug.assert(matches.length > 0); | ||
var bestMatchKind = PatternMatchKind.camelCase; | ||
|
||
for (var i = 0, n = matches.length; i < n; i++) { | ||
var kind = matches[i].kind; | ||
if (kind < bestMatchKind) { | ||
bestMatchKind = kind; | ||
} | ||
} | ||
|
||
return bestMatchKind; | ||
} | ||
|
||
// This means "compare in a case insensitive manner." | ||
var baseSensitivity: Intl.CollatorOptions = { sensitivity: "base" }; | ||
function compareNavigateToItems(i1: RawNavigateToItem, i2: RawNavigateToItem) { | ||
|
@@ -62,56 +196,14 @@ module ts.NavigateTo { | |
name: rawItem.name, | ||
kind: getNodeKind(declaration), | ||
kindModifiers: getNodeModifiers(declaration), | ||
matchKind: MatchKind[rawItem.matchKind], | ||
matchKind: PatternMatchKind[rawItem.matchKind], | ||
isCaseSensitive: rawItem.isCaseSensitive, | ||
fileName: rawItem.fileName, | ||
textSpan: createTextSpanFromBounds(declaration.getStart(), declaration.getEnd()), | ||
// TODO(jfreeman): What should be the containerName when the container has a computed name? | ||
containerName: container && container.name ? (<Identifier>container.name).text : "", | ||
containerKind: container && container.name ? getNodeKind(container) : "" | ||
}; | ||
} | ||
|
||
function hasAnyUpperCaseCharacter(s: string): boolean { | ||
for (var i = 0, n = s.length; i < n; i++) { | ||
var c = s.charCodeAt(i); | ||
if ((CharacterCodes.A <= c && c <= CharacterCodes.Z) || | ||
(c >= CharacterCodes.maxAsciiCharacter && s.charAt(i).toLocaleLowerCase() !== s.charAt(i))) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
function getMatchKind(searchTerms: { caseSensitive: boolean; term: string }[], name: string): MatchKind { | ||
var matchKind = MatchKind.none; | ||
|
||
if (name) { | ||
for (var j = 0, n = searchTerms.length; j < n; j++) { | ||
var searchTerm = searchTerms[j]; | ||
var nameToSearch = searchTerm.caseSensitive ? name : name.toLocaleLowerCase(); | ||
// in case of case-insensitive search searchTerm.term will already be lower-cased | ||
var index = nameToSearch.indexOf(searchTerm.term); | ||
if (index < 0) { | ||
// Didn't match. | ||
return MatchKind.none; | ||
} | ||
|
||
var termKind = MatchKind.substring; | ||
if (index === 0) { | ||
// here we know that match occur at the beginning of the string. | ||
// if search term and declName has the same length - we have an exact match, otherwise declName have longer length and this will be prefix match | ||
termKind = name.length === searchTerm.term.length ? MatchKind.exact : MatchKind.prefix; | ||
} | ||
|
||
// Update our match kind if we don't have one, or if this match is better. | ||
if (matchKind === MatchKind.none || termKind < matchKind) { | ||
matchKind = termKind; | ||
} | ||
} | ||
} | ||
|
||
return matchKind; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have to do this now for every language service API change we make?