Merge pull request #2396 from Microsoft/completionsInIncompleteConstructs

Better completions in incomplete constructs
This commit is contained in:
Daniel Rosenwasser
2015-03-18 14:11:05 -07:00
68 changed files with 1039 additions and 133 deletions

View File

@@ -620,7 +620,7 @@ module FourSlash {
this.scenarioActions.push('<VerifyCompletionDoesNotContainItem ItemName="' + escapeXmlAttributeValue(symbol) + '" />');
var members = this.getMemberListAtCaret();
if (members.entries.filter(e => e.name === symbol).length !== 0) {
if (members && members.entries.filter(e => e.name === symbol).length !== 0) {
this.raiseError('Member list did contain ' + symbol);
}
}
@@ -691,7 +691,12 @@ module FourSlash {
public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string) {
var completions = this.getCompletionListAtCaret();
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind);
if (completions) {
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind);
}
else {
this.raiseError(`No completions at position '${ this.currentCaretPosition }' when looking for '${ symbol }'.`);
}
}
public verifyCompletionListDoesNotContain(symbol: string) {
@@ -699,7 +704,7 @@ module FourSlash {
this.scenarioActions.push('<VerifyCompletionDoesNotContainItem ItemName="' + escapeXmlAttributeValue(symbol) + '" />');
var completions = this.getCompletionListAtCaret();
if (completions && completions.entries && completions.entries.filter(e => e.name === symbol).length !== 0) {
if (completions && completions.entries.filter(e => e.name === symbol).length !== 0) {
this.raiseError('Completion list did contain ' + symbol);
}
}

View File

@@ -215,11 +215,7 @@ module ts.formatting {
function getStartLineAndCharacterForNode(n: Node, sourceFile: SourceFile): LineAndCharacter {
return sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile));
}
function positionBelongsToNode(candidate: Node, position: number, sourceFile: SourceFile): boolean {
return candidate.end > position || !isCompletedNode(candidate, sourceFile);
}
export function childStartsOnTheSameLineWithElseInIfStatement(parent: Node, child: TextRangeWithKind, childStartLine: number, sourceFile: SourceFile): boolean {
if (parent.kind === SyntaxKind.IfStatement && (<IfStatement>parent).elseStatement === child) {
let elseKeyword = findChildOfKind(parent, SyntaxKind.ElseKeyword, sourceFile);
@@ -403,128 +399,5 @@ module ts.formatting {
return false;
}
}
/*
* Checks if node ends with 'expectedLastToken'.
* If child at position 'length - 1' is 'SemicolonToken' it is skipped and 'expectedLastToken' is compared with child at position 'length - 2'.
*/
function nodeEndsWith(n: Node, expectedLastToken: SyntaxKind, sourceFile: SourceFile): boolean {
let children = n.getChildren(sourceFile);
if (children.length) {
let last = children[children.length - 1];
if (last.kind === expectedLastToken) {
return true;
}
else if (last.kind === SyntaxKind.SemicolonToken && children.length !== 1) {
return children[children.length - 2].kind === expectedLastToken;
}
}
return false;
}
/*
* This function is always called when position of the cursor is located after the node
*/
function isCompletedNode(n: Node, sourceFile: SourceFile): boolean {
if (n.getFullWidth() === 0) {
return false;
}
switch (n.kind) {
case SyntaxKind.ClassDeclaration:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.EnumDeclaration:
case SyntaxKind.ObjectLiteralExpression:
case SyntaxKind.ObjectBindingPattern:
case SyntaxKind.TypeLiteral:
case SyntaxKind.Block:
case SyntaxKind.ModuleBlock:
case SyntaxKind.CaseBlock:
return nodeEndsWith(n, SyntaxKind.CloseBraceToken, sourceFile);
case SyntaxKind.CatchClause:
return isCompletedNode((<CatchClause>n).block, sourceFile);
case SyntaxKind.NewExpression:
if (!(<NewExpression>n).arguments) {
return true;
}
// fall through
case SyntaxKind.CallExpression:
case SyntaxKind.ParenthesizedExpression:
case SyntaxKind.ParenthesizedType:
return nodeEndsWith(n, SyntaxKind.CloseParenToken, sourceFile);
case SyntaxKind.FunctionType:
case SyntaxKind.ConstructorType:
return isCompletedNode((<SignatureDeclaration>n).type, sourceFile);
case SyntaxKind.Constructor:
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.MethodSignature:
case SyntaxKind.ConstructSignature:
case SyntaxKind.CallSignature:
case SyntaxKind.ArrowFunction:
if ((<FunctionLikeDeclaration>n).body) {
return isCompletedNode((<FunctionLikeDeclaration>n).body, sourceFile);
}
if ((<FunctionLikeDeclaration>n).type) {
return isCompletedNode((<FunctionLikeDeclaration>n).type, sourceFile);
}
// Even though type parameters can be unclosed, we can get away with
// having at least a closing paren.
return hasChildOfKind(n, SyntaxKind.CloseParenToken, sourceFile);
case SyntaxKind.ModuleDeclaration:
return (<ModuleDeclaration>n).body && isCompletedNode((<ModuleDeclaration>n).body, sourceFile);
case SyntaxKind.IfStatement:
if ((<IfStatement>n).elseStatement) {
return isCompletedNode((<IfStatement>n).elseStatement, sourceFile);
}
return isCompletedNode((<IfStatement>n).thenStatement, sourceFile);
case SyntaxKind.ExpressionStatement:
return isCompletedNode((<ExpressionStatement>n).expression, sourceFile);
case SyntaxKind.ArrayLiteralExpression:
case SyntaxKind.ArrayBindingPattern:
case SyntaxKind.ComputedPropertyName:
case SyntaxKind.TupleType:
return nodeEndsWith(n, SyntaxKind.CloseBracketToken, sourceFile);
case SyntaxKind.IndexSignature:
if ((<IndexSignatureDeclaration>n).type) {
return isCompletedNode((<IndexSignatureDeclaration>n).type, sourceFile);
}
return hasChildOfKind(n, SyntaxKind.CloseBracketToken, sourceFile);
case SyntaxKind.CaseClause:
case SyntaxKind.DefaultClause:
// there is no such thing as terminator token for CaseClause/DefaultClause so for simplicitly always consider them non-completed
return false;
case SyntaxKind.ForStatement:
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement:
case SyntaxKind.WhileStatement:
return isCompletedNode((<IterationStatement>n).statement, sourceFile);
case SyntaxKind.DoStatement:
// rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')';
let hasWhileKeyword = findChildOfKind(n, SyntaxKind.WhileKeyword, sourceFile);
if (hasWhileKeyword) {
return nodeEndsWith(n, SyntaxKind.CloseParenToken, sourceFile);
}
return isCompletedNode((<DoStatement>n).statement, sourceFile);
default:
return true;
}
}
}
}

View File

@@ -2597,8 +2597,9 @@ module ts {
isNewIdentifierLocation = isNewIdentifierDefinitionLocation(previousToken);
/// TODO filter meaning based on the current context
let scopeNode = getScopeNode(previousToken, position, sourceFile);
let symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias;
let symbols = typeInfoResolver.getSymbolsInScope(node, symbolMeanings);
let symbols = typeInfoResolver.getSymbolsInScope(scopeNode, symbolMeanings);
getCompletionEntriesFromSymbols(symbols, activeCompletionSession);
}
@@ -2613,10 +2614,22 @@ module ts {
return {
isMemberCompletion,
isNewIdentifierLocation,
isBuilder : isNewIdentifierDefinitionLocation, // temporary property used to match VS implementation
isBuilder: isNewIdentifierDefinitionLocation, // temporary property used to match VS implementation
entries: activeCompletionSession.entries
};
/**
* Finds the first node that "embraces" the position, so that one may
* accurately aggregate locals from the closest containing scope.
*/
function getScopeNode(initialToken: Node, position: number, sourceFile: SourceFile) {
var scope = initialToken;
while (scope && !positionBelongsToNode(scope, position, sourceFile)) {
scope = scope.parent;
}
return scope;
}
function getCompletionEntriesFromSymbols(symbols: Symbol[], session: CompletionSession): void {
let start = new Date().getTime();
forEach(symbols, symbol => {

View File

@@ -59,6 +59,157 @@ module ts {
return start < end;
}
export function positionBelongsToNode(candidate: Node, position: number, sourceFile: SourceFile): boolean {
return candidate.end > position || !isCompletedNode(candidate, sourceFile);
}
export function isCompletedNode(n: Node, sourceFile: SourceFile): boolean {
if (nodeIsMissing(n)) {
return false;
}
switch (n.kind) {
case SyntaxKind.ClassDeclaration:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.EnumDeclaration:
case SyntaxKind.ObjectLiteralExpression:
case SyntaxKind.ObjectBindingPattern:
case SyntaxKind.TypeLiteral:
case SyntaxKind.Block:
case SyntaxKind.ModuleBlock:
case SyntaxKind.CaseBlock:
return nodeEndsWith(n, SyntaxKind.CloseBraceToken, sourceFile);
case SyntaxKind.CatchClause:
return isCompletedNode((<CatchClause>n).block, sourceFile);
case SyntaxKind.NewExpression:
if (!(<NewExpression>n).arguments) {
return true;
}
// fall through
case SyntaxKind.CallExpression:
case SyntaxKind.ParenthesizedExpression:
case SyntaxKind.ParenthesizedType:
return nodeEndsWith(n, SyntaxKind.CloseParenToken, sourceFile);
case SyntaxKind.FunctionType:
case SyntaxKind.ConstructorType:
return isCompletedNode((<SignatureDeclaration>n).type, sourceFile);
case SyntaxKind.Constructor:
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.MethodSignature:
case SyntaxKind.ConstructSignature:
case SyntaxKind.CallSignature:
case SyntaxKind.ArrowFunction:
if ((<FunctionLikeDeclaration>n).body) {
return isCompletedNode((<FunctionLikeDeclaration>n).body, sourceFile);
}
if ((<FunctionLikeDeclaration>n).type) {
return isCompletedNode((<FunctionLikeDeclaration>n).type, sourceFile);
}
// Even though type parameters can be unclosed, we can get away with
// having at least a closing paren.
return hasChildOfKind(n, SyntaxKind.CloseParenToken, sourceFile);
case SyntaxKind.ModuleDeclaration:
return (<ModuleDeclaration>n).body && isCompletedNode((<ModuleDeclaration>n).body, sourceFile);
case SyntaxKind.IfStatement:
if ((<IfStatement>n).elseStatement) {
return isCompletedNode((<IfStatement>n).elseStatement, sourceFile);
}
return isCompletedNode((<IfStatement>n).thenStatement, sourceFile);
case SyntaxKind.ExpressionStatement:
return isCompletedNode((<ExpressionStatement>n).expression, sourceFile);
case SyntaxKind.ArrayLiteralExpression:
case SyntaxKind.ArrayBindingPattern:
case SyntaxKind.ElementAccessExpression:
case SyntaxKind.ComputedPropertyName:
case SyntaxKind.TupleType:
return nodeEndsWith(n, SyntaxKind.CloseBracketToken, sourceFile);
case SyntaxKind.IndexSignature:
if ((<IndexSignatureDeclaration>n).type) {
return isCompletedNode((<IndexSignatureDeclaration>n).type, sourceFile);
}
return hasChildOfKind(n, SyntaxKind.CloseBracketToken, sourceFile);
case SyntaxKind.CaseClause:
case SyntaxKind.DefaultClause:
// there is no such thing as terminator token for CaseClause/DefaultClause so for simplicitly always consider them non-completed
return false;
case SyntaxKind.ForStatement:
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement:
case SyntaxKind.WhileStatement:
return isCompletedNode((<IterationStatement>n).statement, sourceFile);
case SyntaxKind.DoStatement:
// rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')';
let hasWhileKeyword = findChildOfKind(n, SyntaxKind.WhileKeyword, sourceFile);
if (hasWhileKeyword) {
return nodeEndsWith(n, SyntaxKind.CloseParenToken, sourceFile);
}
return isCompletedNode((<DoStatement>n).statement, sourceFile);
case SyntaxKind.TypeQuery:
return isCompletedNode((<TypeQueryNode>n).exprName, sourceFile);
case SyntaxKind.TypeOfExpression:
case SyntaxKind.DeleteExpression:
case SyntaxKind.VoidExpression:
case SyntaxKind.YieldExpression:
case SyntaxKind.SpreadElementExpression:
let unaryWordExpression = (<TypeOfExpression|DeleteExpression|VoidExpression|YieldExpression|SpreadElementExpression>n);
return isCompletedNode(unaryWordExpression.expression, sourceFile);
case SyntaxKind.TaggedTemplateExpression:
return isCompletedNode((<TaggedTemplateExpression>n).template, sourceFile);
case SyntaxKind.TemplateExpression:
let lastSpan = lastOrUndefined((<TemplateExpression>n).templateSpans);
return isCompletedNode(lastSpan, sourceFile);
case SyntaxKind.TemplateSpan:
return nodeIsPresent((<TemplateSpan>n).literal);
case SyntaxKind.PrefixUnaryExpression:
return isCompletedNode((<PrefixUnaryExpression>n).operand, sourceFile);
case SyntaxKind.BinaryExpression:
return isCompletedNode((<BinaryExpression>n).right, sourceFile);
case SyntaxKind.ConditionalExpression:
return isCompletedNode((<ConditionalExpression>n).whenFalse, sourceFile);
default:
return true;
}
}
/*
* Checks if node ends with 'expectedLastToken'.
* If child at position 'length - 1' is 'SemicolonToken' it is skipped and 'expectedLastToken' is compared with child at position 'length - 2'.
*/
function nodeEndsWith(n: Node, expectedLastToken: SyntaxKind, sourceFile: SourceFile): boolean {
let children = n.getChildren(sourceFile);
if (children.length) {
let last = children[children.length - 1];
if (last.kind === expectedLastToken) {
return true;
}
else if (last.kind === SyntaxKind.SemicolonToken && children.length !== 1) {
return children[children.length - 2].kind === expectedLastToken;
}
}
return false;
}
export function findListItemInfo(node: Node): ListItemInfo {
let list = findContainingList(node);