136 lines
5.7 KiB
TypeScript
136 lines
5.7 KiB
TypeScript
|
/**
|
||
|
* @license
|
||
|
* Copyright Google LLC All Rights Reserved.
|
||
|
*
|
||
|
* Use of this source code is governed by an MIT-style license that can be
|
||
|
* found in the LICENSE file at https://angular.io/license
|
||
|
*/
|
||
|
|
||
|
import {MethodCall, SafeMethodCall} from '@angular/compiler';
|
||
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
||
|
import {getSourceFileOrError} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||
|
import {SymbolKind} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||
|
import * as ts from 'typescript/lib/tsserverlibrary';
|
||
|
|
||
|
import {getTargetAtPosition, TargetNodeKind} from './template_target';
|
||
|
import {findTightestNode} from './ts_utils';
|
||
|
import {getTemplateInfoAtPosition} from './utils';
|
||
|
|
||
|
/**
|
||
|
* Queries the TypeScript Language Service to get signature help for a template position.
|
||
|
*/
|
||
|
export function getSignatureHelp(
|
||
|
compiler: NgCompiler, tsLS: ts.LanguageService, fileName: string, position: number,
|
||
|
options: ts.SignatureHelpItemsOptions|undefined): ts.SignatureHelpItems|undefined {
|
||
|
const templateInfo = getTemplateInfoAtPosition(fileName, position, compiler);
|
||
|
if (templateInfo === undefined) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
const targetInfo = getTargetAtPosition(templateInfo.template, position);
|
||
|
if (targetInfo === null) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
if (targetInfo.context.kind !== TargetNodeKind.RawExpression &&
|
||
|
targetInfo.context.kind !== TargetNodeKind.MethodCallExpressionInArgContext) {
|
||
|
// Signature completions are only available in expressions.
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
const symbol = compiler.getTemplateTypeChecker().getSymbolOfNode(
|
||
|
targetInfo.context.node, templateInfo.component);
|
||
|
if (symbol === null || symbol.kind !== SymbolKind.Expression) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
// Determine a shim position to use in the request to the TypeScript Language Service.
|
||
|
// Additionally, extract the `MethodCall` or `SafeMethodCall` node for which signature help is
|
||
|
// being queried, as this is needed to construct the correct span for the results later.
|
||
|
let shimPosition: number;
|
||
|
let expr: MethodCall|SafeMethodCall;
|
||
|
switch (targetInfo.context.kind) {
|
||
|
case TargetNodeKind.RawExpression:
|
||
|
// For normal expressions, just use the primary TCB position of the expression.
|
||
|
shimPosition = symbol.shimLocation.positionInShimFile;
|
||
|
|
||
|
// Walk up the parents of this expression and try to find a `MethodCall` or `SafeMethodCall`
|
||
|
// for which signature information is being fetched.
|
||
|
let callExpr: MethodCall|SafeMethodCall|null = null;
|
||
|
const parents = targetInfo.context.parents;
|
||
|
for (let i = parents.length - 1; i >= 0; i--) {
|
||
|
const parent = parents[i];
|
||
|
if (parent instanceof MethodCall || parent instanceof SafeMethodCall) {
|
||
|
callExpr = parent;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If no MethodCall or SafeMethodCall node could be found, then this query cannot be safely
|
||
|
// answered as a correct span for the results will not be obtainable.
|
||
|
if (callExpr === null) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
expr = callExpr;
|
||
|
break;
|
||
|
case TargetNodeKind.MethodCallExpressionInArgContext:
|
||
|
// The `Symbol` points to a `MethodCall` or `SafeMethodCall` expression in the TCB (where it
|
||
|
// will be represented as a `ts.CallExpression`) *and* the template position was within the
|
||
|
// argument list of the method call. This happens when there was no narrower expression inside
|
||
|
// the argument list that matched the template position, such as when the call has no
|
||
|
// arguments: `foo(|)`.
|
||
|
//
|
||
|
// The `Symbol`'s shim position is to the start of the call expression (`|foo()`) and
|
||
|
// therefore wouldn't return accurate signature help from the TS language service. For that, a
|
||
|
// position within the argument list for the `ts.CallExpression` in the TCB will need to be
|
||
|
// determined. This is done by finding that call expression and extracting a viable position
|
||
|
// from it directly.
|
||
|
//
|
||
|
// First, use `findTightestNode` to locate the `ts.Node` at `symbol`'s location.
|
||
|
const shimSf =
|
||
|
getSourceFileOrError(compiler.getCurrentProgram(), symbol.shimLocation.shimPath);
|
||
|
let shimNode: ts.Node|null =
|
||
|
findTightestNode(shimSf, symbol.shimLocation.positionInShimFile) ?? null;
|
||
|
|
||
|
// This node should be somewhere inside a `ts.CallExpression`. Walk up the AST to find it.
|
||
|
while (shimNode !== null) {
|
||
|
if (ts.isCallExpression(shimNode)) {
|
||
|
break;
|
||
|
}
|
||
|
shimNode = shimNode.parent ?? null;
|
||
|
}
|
||
|
|
||
|
// If one couldn't be found, something is wrong, so bail rather than report incorrect results.
|
||
|
if (shimNode === null || !ts.isCallExpression(shimNode)) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
// Position the cursor in the TCB at the start of the argument list for the
|
||
|
// `ts.CallExpression`. This will allow us to get the correct signature help, even though the
|
||
|
// template itself doesn't have an expression inside the argument list.
|
||
|
shimPosition = shimNode.arguments.pos;
|
||
|
|
||
|
// In this case, getting the right call AST node is easy.
|
||
|
expr = targetInfo.context.node;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
const res = tsLS.getSignatureHelpItems(symbol.shimLocation.shimPath, shimPosition, options);
|
||
|
if (res === undefined) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
// The TS language service results are almost returnable as-is. However, they contain an
|
||
|
// `applicableSpan` which marks the entire argument list, and that span is in the context of the
|
||
|
// TCB's `ts.CallExpression`. It needs to be replaced with the span for the `MethodCall` (or
|
||
|
// `SafeMethodCall`) argument list.
|
||
|
return {
|
||
|
...res,
|
||
|
applicableSpan: {
|
||
|
start: expr.argumentSpan.start,
|
||
|
length: expr.argumentSpan.end - expr.argumentSpan.start,
|
||
|
},
|
||
|
};
|
||
|
}
|