Skip to content

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

Merged
merged 3 commits into from
Feb 24, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ module ts.server {
kind: entry.kind,
kindModifiers: entry.kindModifiers,
matchKind: entry.matchKind,
isCaseSensitive: entry.isCaseSensitive,
Copy link
Contributor

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?

fileName: fileName,
textSpan: ts.createTextSpanFromBounds(start, end)
};
Expand Down
5 changes: 5 additions & 0 deletions src/server/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,11 @@ declare module ts.server.protocol {
* exact, substring, or prefix.
*/
matchKind?: string;

/**
* If this was a case sensitive or insensitive match.
*/
isCaseSensitive?: boolean;

/**
* Optional modifiers for the kind (such as 'public').
Expand Down
222 changes: 157 additions & 65 deletions src/services/navigateTo.ts
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 };
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't getMatches in the pattern matcher already do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 });
}
}
});
Expand All @@ -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) {
Expand All @@ -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;
}
}
}
Loading