diff --git a/src/compiler/core.ts b/src/compiler/core.ts index c4aafbe2ef9..fb2bde52453 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -41,6 +41,18 @@ module ts { return -1; } + export function countWhere(array: T[], predicate: (x: T) => boolean): number { + var count = 0; + if (array) { + for (var i = 0, len = array.length; i < len; i++) { + if (predicate(array[i])) { + count++; + } + } + } + return count; + } + export function filter(array: T[], f: (x: T) => boolean): T[] { if (array) { var result: T[] = []; diff --git a/src/services/services.ts b/src/services/services.ts index 170525e8887..3a6be75a27a 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -3479,8 +3479,36 @@ module ts { return emitOutput; } + function getChildListThatStartsWithOpenerToken(parent: Node, openerToken: Node, sourceFile: SourceFile): Node { + var children = parent.getChildren(sourceFile); + var indexOfOpenerToken = children.indexOf(openerToken); + return children[indexOfOpenerToken + 1]; + } + // Signature help function getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems { + synchronizeHostData(); + + // Decide whether to show signature help + fileName = TypeScript.switchToForwardSlashes(fileName); + var sourceFile = getSourceFile(fileName); + var node = getNodeAtPosition(sourceFile, position); + + + // Semantic filtering of signature help + var signatureHelpContext = getSignatureHelpArgumentContext(node); + if (signatureHelpContext) { + var call = signatureHelpContext.list.parent; + var candidates = []; + var resolvedSignature = typeInfoResolver.getResolvedSignature(call, candidates); + return candidates.length + ? createSignatureHelpItems(candidates, resolvedSignature, signatureHelpContext.list) + : undefined; + } + + return undefined; + + // If node is an argument, returns its index in the argument list // If not, returns -1 function getArgumentIndexInfo(node: Node): ServicesSyntaxUtilities.ListItemInfo { @@ -3492,16 +3520,7 @@ module ts { // Find out if 'node' is an argument, a type argument, or neither if (node.kind === SyntaxKind.LessThanToken || node.kind === SyntaxKind.OpenParenToken) { // Find the list that starts right *after* the < or ( token - var seenRelevantOpenerToken = false; - var list = forEach(parent.getChildren(), c => { - if (seenRelevantOpenerToken) { - Debug.assert(c.kind === SyntaxKind.SyntaxList); - return c; - } - if (c.kind === node.kind /*node is the relevant opener token we are looking for*/) { - seenRelevantOpenerToken = true; - } - }); + var list = getChildListThatStartsWithOpenerToken(parent, node, sourceFile); Debug.assert(list); // Treat the open paren / angle bracket of a call as the introduction of parameter slot 0 return { @@ -3549,7 +3568,7 @@ module ts { return undefined; } - function getSignatureHelpItemsFromCandidateInfo(candidates: Signature[], bestSignature: Signature, argumentListOrTypeArgumentList: Node): SignatureHelpItems { + function createSignatureHelpItems(candidates: Signature[], bestSignature: Signature, argumentListOrTypeArgumentList: Node): SignatureHelpItems { var items = map(candidates, candidateSignature => { var parameters = candidateSignature.parameters; var parameterHelpItems = parameters.length === 0 ? emptyArray : map(parameters, p => { @@ -3586,32 +3605,49 @@ module ts { var applicableSpan = new TypeScript.TextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); return new SignatureHelpItems(items, applicableSpan, selectedItemIndex); } - - synchronizeHostData(); - - // Decide whether to show signature help - fileName = TypeScript.switchToForwardSlashes(fileName); - var sourceFile = getSourceFile(fileName); - var node = getNodeAtPosition(sourceFile, position); - - - // Semantic filtering of signature help - var signatureHelpContext = getSignatureHelpArgumentContext(node); - if (signatureHelpContext) { - var call = signatureHelpContext.list.parent; - var candidates = []; - var resolvedSignature = typeInfoResolver.getResolvedSignature(call, candidates); - return candidates.length - ? getSignatureHelpItemsFromCandidateInfo(candidates, resolvedSignature, signatureHelpContext.list) - : undefined; - } - - return undefined; } function getSignatureHelpCurrentArgumentState(fileName: string, position: number, applicableSpanStart: number): SignatureHelpState { - synchronizeHostData(); - return null; + fileName = TypeScript.switchToForwardSlashes(fileName); + var sourceFile = getCurrentSourceFile(fileName); + var tokenPrecedingSpanStart = ServicesSyntaxUtilities.findPrecedingToken(applicableSpanStart, sourceFile); + if (tokenPrecedingSpanStart.kind !== SyntaxKind.OpenParenToken && tokenPrecedingSpanStart.kind !== SyntaxKind.LessThanToken) { + // The span start must have moved backward in the file (for example if the open paren was backspaced) + return undefined; + } + + var tokenPrecedingCurrentPosition = ServicesSyntaxUtilities.findPrecedingToken(position, sourceFile); + var call = tokenPrecedingSpanStart.parent; + if (tokenPrecedingCurrentPosition.kind === SyntaxKind.CloseParenToken || tokenPrecedingCurrentPosition.kind === SyntaxKind.GreaterThanToken) { + if (tokenPrecedingCurrentPosition.parent === call) { + // This call expression is complete. Stop signature help. + return undefined; + } + // TODO(jfreeman): handle other (incorrect) ways that a call expression can end + } + + Debug.assert(call.kind === SyntaxKind.CallExpression || call.kind === SyntaxKind.NewExpression, "wrong call kind " + SyntaxKind[call.kind]); + + var argumentListOrTypeArgumentList = getChildListThatStartsWithOpenerToken(call, tokenPrecedingSpanStart, sourceFile); + // Debug.assert(argumentListOrTypeArgumentList.getChildCount() === 0 || argumentListOrTypeArgumentList.getChildCount() % 2 === 1, "Even number of children"); + + var numberOfCommas = countWhere(argumentListOrTypeArgumentList.getChildren(), arg => arg.kind === SyntaxKind.CommaToken); + var argumentCount = numberOfCommas + 1; + + + if (argumentCount <= 1) { + return new SignatureHelpState(/*argumentIndex*/ 0, argumentCount); + } + + var indexOfNodeContainingPosition = ServicesSyntaxUtilities.findListItemIndexContainingPosition(argumentListOrTypeArgumentList, position); + + // indexOfNodeContainingPosition checks that position is between pos and end of each child, so it is + // possible that we are to the right of all children. Assume that we are still within + // the applicable span and that we are typing the last argument + // Alternatively, we could be in range of one of the arguments, in which case we need to divide + // by 2 to exclude commas + var argumentIndex = indexOfNodeContainingPosition < 0 ? argumentCount - 1 : indexOfNodeContainingPosition / 2; + return new SignatureHelpState(argumentIndex, argumentCount); } /// Syntactic features diff --git a/src/services/servicesSyntaxUtilities.ts b/src/services/servicesSyntaxUtilities.ts index 5c72428bd04..b7e58ebe567 100644 --- a/src/services/servicesSyntaxUtilities.ts +++ b/src/services/servicesSyntaxUtilities.ts @@ -26,6 +26,19 @@ module ts.ServicesSyntaxUtilities { }; } + // Includes the start position of each child, but excludes the end + export function findListItemIndexContainingPosition(list: Node, position: number): number { + Debug.assert(list.kind === SyntaxKind.SyntaxList); + var children = list.getChildren(); + for (var i = 0; i < children.length; i++) { + if (children[i].pos <= position && children[i].end > position) { + return i; + } + } + + return -1; + } + export function findNextToken(previousToken: Node, parent: Node): Node { return find(parent); diff --git a/src/services/shims.ts b/src/services/shims.ts index 210a458b0be..01562c36abe 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -587,8 +587,8 @@ module ts { return this.forwardJSONCall( "getSignatureHelpCurrentArgumentState('" + fileName + "', " + position + ", " + applicableSpanStart + ")", () => { - var signatureInfo = this.languageService.getSignatureHelpItems(fileName, position); - return signatureInfo; + var signatureHelpState = this.languageService.getSignatureHelpCurrentArgumentState(fileName, position, applicableSpanStart); + return signatureHelpState; }); }