From 34087bd9ec9a99a0cda613768356256f2a17ffa3 Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Mon, 17 Nov 2014 15:13:58 -0800 Subject: [PATCH] Refactored code, adjusted for residing out of bounds of the template. --- src/compiler/parser.ts | 2 +- src/services/signatureHelp.ts | 142 +++++++++++++++++++++++----------- src/services/utilities.ts | 5 ++ 3 files changed, 103 insertions(+), 46 deletions(-) diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index a3626caa0a3..804f8c9780c 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -811,7 +811,7 @@ module ts { } export function isUnterminatedTemplateEnd(node: LiteralExpression) { - Debug.assert(node.kind === SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === SyntaxKind.TemplateTail); + Debug.assert(isTemplateLiteralKind(node.kind)); var sourceText = getSourceFileOfNode(node).text; // If we're not at the EOF, we know we must be terminated. diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index fc62ff34d0a..bba70da4627 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -173,7 +173,7 @@ module ts.SignatureHelp { export interface ArgumentListInfo { kind: ArgumentListKind; invocation: CallLikeExpression; - argumentRange: TextRange; + argumentsSpan: TypeScript.TextSpan; argumentIndex?: number; argumentCount: number; } @@ -186,7 +186,7 @@ module ts.SignatureHelp { return undefined; } - var argumentInfo = getContainingArgumentInfo(startingToken); + var argumentInfo = getContainingArgumentInfo(startingToken, position); cancellationToken.throwIfCancellationRequested(); // Semantic filtering of signature help @@ -209,7 +209,7 @@ module ts.SignatureHelp { * If node is an argument, returns its index in the argument list. * If not, returns -1. */ - function getImmediatelyContainingArgumentInfo(node: Node): ArgumentListInfo { + function getImmediatelyContainingArgumentInfo(node: Node, position: number): ArgumentListInfo { var callLikeExpr: CallLikeExpression; if (node.parent.kind === SyntaxKind.CallExpression || node.parent.kind === SyntaxKind.NewExpression) { @@ -238,7 +238,7 @@ module ts.SignatureHelp { return { kind: isTypeArgList ? ArgumentListKind.TypeArguments : ArgumentListKind.CallArguments, invocation: callExpression, - argumentRange: list, + argumentsSpan: getApplicableSpanForArguments(list), argumentIndex: 0, argumentCount: getCommaBasedArgCount(list) }; @@ -269,23 +269,25 @@ module ts.SignatureHelp { return { kind: isTypeArgList ? ArgumentListKind.TypeArguments : ArgumentListKind.CallArguments, invocation: callExpression, - argumentRange: list, + argumentsSpan: getApplicableSpanForArguments(list), argumentIndex: argumentIndex, argumentCount: getCommaBasedArgCount(list) }; } } else if (node.kind === SyntaxKind.NoSubstitutionTemplateLiteral && node.parent.kind === SyntaxKind.TaggedTemplateExpression) { - return getArgumentListInfoForTemplate(node.parent, /*argumentIndex*/ 0); + // Check if we're actually inside the template; + // otherwise we'll fall out and return undefined. + if (isInsideTemplateLiteral(node, position)) { + return getArgumentListInfoForTemplate(node.parent, /*argumentIndex*/ 0); + } } - else if (node.parent.kind === SyntaxKind.TemplateExpression && node.parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { - Debug.assert(node.kind === SyntaxKind.TemplateHead, "Expected 'TemplateHead' as token."); - + else if (node.kind === SyntaxKind.TemplateHead && node.parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { var templateExpression = node.parent; var tagExpression = templateExpression.parent; + var argumentIndex = getArgumentIndexForTemplatePiece(/*spanIndex*/ 0, node, position); - // argumentIndex is 1 to adjust for the TemplateStringsArray - return getArgumentListInfoForTemplate(tagExpression, /*argumentIndex*/ 1); + return getArgumentListInfoForTemplate(tagExpression, argumentIndex); } else if (node.parent.kind === SyntaxKind.TemplateSpan && node.parent.parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { var templateSpan = node.parent; @@ -293,31 +295,25 @@ module ts.SignatureHelp { var tagExpression = templateExpression.parent; Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression); - // We need to account for the TemplateStringsArray, so we add at least 1. - // Then, if we're on the template literal, we want to jump to the next argument, - var spanIndex = templateExpression.templateSpans.indexOf(templateSpan); - var adjustedIndex = isTemplateLiteralKind(node.kind) ? spanIndex + 2 : spanIndex + 1 + // If we're just after a template tail, don't show signature help. + if (node.kind === SyntaxKind.TemplateTail && position >= node.getEnd() && !isUnterminatedTemplateEnd(node)) { + return undefined; + } - return getArgumentListInfoForTemplate(tagExpression, adjustedIndex); + // The cursor can either be in or after the template literal. + // If inside, we want to highlight the first argument. + // If after, we'll want to highlight the next parameter. + // We actually shouldn't be able to show up before because you should be at the left-most token. + + var spanIndex = templateExpression.templateSpans.indexOf(templateSpan); + var argumentIndex = getArgumentIndexForTemplatePiece(spanIndex, node, position); + + return getArgumentListInfoForTemplate(tagExpression, argumentIndex); } return undefined; } - function getArgumentListInfoForTemplate(tagExpression: TaggedTemplateExpression, argumentIndex: number): ArgumentListInfo { - var argumentCount = tagExpression.template.kind === SyntaxKind.NoSubstitutionTemplateLiteral - ? 1 - : (tagExpression.template).templateSpans.length + 1; - - return { - kind: ArgumentListKind.TaggedTemplateArguments, - invocation: tagExpression, - argumentRange: tagExpression.template, - argumentIndex: argumentIndex, - argumentCount: argumentCount - }; - } - function getCommaBasedArgCount(argumentsList: Node) { // The number of arguments is the number of commas plus one, unless the list // is completely empty, in which case there are 0 arguments. @@ -326,7 +322,75 @@ module ts.SignatureHelp { : 1 + countWhere(argumentsList.getChildren(), arg => arg.kind === SyntaxKind.CommaToken); } - function getContainingArgumentInfo(node: Node): ArgumentListInfo { + // spanIndex is either the index for a given template span, or 0 for a template head. + // This does not give appropriate results for a NoSubstitutionTemplateLiteral + function getArgumentIndexForTemplatePiece(spanIndex: number, node: Node, position: number): number { + // TemplateSpans are expression-template pairs, and ordered as such. + // Because of the TemplateStringsArray arg, we have to offset ourselves by 1 for substitution expressions. + // There are three cases we can encounter: + // 1. We are precisely in the template literal (argIndex = 0) + // 2. We are in or to the right of the substitution expression (argIndex = spanIndex + 1) + // 3. We are directly to the right of the template literal, but not + // enough to put us in the substitution expression; we should consider ourselves part of + // the *next* span's expression (argIndex = (spanIndex + 1) + 1). + // + // Example: f `# abcd $#{# 1 + 1# }# efghi ${ #"#hello"# } # ` + // ^ ^ ^ ^ ^ ^ ^ ^ ^ + // Cases: 1 1 3 2 1 3 2 2 1 + Debug.assert(position >= node.getStart(), "Assumed 'position' could not occur before node."); + if (isTemplateLiteralKind(node.kind)) { + if (isInsideTemplateLiteral(node, position)) { + return 0; + } + return spanIndex + 2; + } + return spanIndex + 1; + } + + function getArgumentListInfoForTemplate(tagExpression: TaggedTemplateExpression, argumentIndex: number): ArgumentListInfo { + var argumentCount = tagExpression.template.kind === SyntaxKind.NoSubstitutionTemplateLiteral + ? 1 + : (tagExpression.template).templateSpans.length + 1; + + return { + kind: ArgumentListKind.TaggedTemplateArguments, + invocation: tagExpression, + argumentsSpan: getApplicableSpanForTaggedTemplate(tagExpression.template), + argumentIndex: argumentIndex, + argumentCount: argumentCount + }; + } + + function getApplicableSpanForArguments(argumentsList: Node): TypeScript.TextSpan { + // We use full start and skip trivia on the end because we want to include trivia on + // both sides. For example, + // + // foo( /*comment */ a, b, c /*comment*/ ) + // | | + // + // The applicable span is from the first bar to the second bar (inclusive, + // but not including parentheses) + var applicableSpanStart = argumentsList.pos; + var applicableSpanEnd = skipTrivia(sourceFile.text, argumentsList.end, /*stopAfterLineBreak*/ false); + return new TypeScript.TextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); + } + + function getApplicableSpanForTaggedTemplate(template: TemplateExpression | LiteralExpression): TypeScript.TextSpan { + var applicableSpanStart = template.getStart(); + var applicableSpanEnd = template.getEnd(); + + // Adjust the span end in case the template is unclosed - + if (template.kind === SyntaxKind.TemplateExpression) { + var lastSpan = lastOrUndefined((template).templateSpans); + if (lastSpan.literal.kind === SyntaxKind.Missing) { + applicableSpanEnd = skipTrivia(sourceFile.text, template.end, /*stopAfterLineBreak*/ false); + } + } + + return new TypeScript.TextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); + } + + function getContainingArgumentInfo(node: Node, position: number): ArgumentListInfo { for (var n = node; n.kind !== SyntaxKind.SourceFile; n = n.parent) { if (n.kind === SyntaxKind.FunctionBlock) { return undefined; @@ -338,7 +402,7 @@ module ts.SignatureHelp { Debug.fail("Node of kind " + n.kind + " is not a subspan of its parent of kind " + n.parent.kind); } - var argumentInfo = getImmediatelyContainingArgumentInfo(n); + var argumentInfo = getImmediatelyContainingArgumentInfo(n, position); if (argumentInfo) { return argumentInfo; } @@ -384,7 +448,7 @@ module ts.SignatureHelp { } function createSignatureHelpItems(candidates: Signature[], bestSignature: Signature, argumentListInfo: ArgumentListInfo): SignatureHelpItems { - var argumentsList = argumentListInfo.argumentRange; + var applicableSpan = argumentListInfo.argumentsSpan; var isTypeParameterList = argumentListInfo.kind === ArgumentListKind.TypeArguments; var invocation = argumentListInfo.invocation; @@ -433,18 +497,6 @@ module ts.SignatureHelp { }; }); - // We use full start and skip trivia on the end because we want to include trivia on - // both sides. For example, - // - // foo( /*comment */ a, b, c /*comment*/ ) - // | | - // - // The applicable span is from the first bar to the second bar (inclusive, - // but not including parentheses) - var applicableSpanStart = argumentsList.pos; - var applicableSpanEnd = skipTrivia(sourceFile.text, argumentsList.end, /*stopAfterLineBreak*/ false); - var applicableSpan = new TypeScript.TextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); - var argumentIndex = argumentListInfo.argumentIndex; // argumentCount is the *apparent* number of arguments. diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 8620f8d1920..ab651491518 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -320,4 +320,9 @@ module ts { export function isPunctuation(kind: SyntaxKind): boolean { return SyntaxKind.FirstPunctuation <= kind && kind <= SyntaxKind.LastPunctuation; } + + export function isInsideTemplateLiteral(node: LiteralExpression, position: number) { + return (node.getStart() < position && position < node.getEnd()) + || (isUnterminatedTemplateEnd(node) && position >= node.getEnd()); + } } \ No newline at end of file