Skip to content

Actual signature help trigger filtering #25422

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 6 commits into from
Jul 9, 2018
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
4 changes: 3 additions & 1 deletion src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1515,7 +1515,9 @@ Actual: ${stringify(fullActual)}`);
"argumentCount",
];
for (const key in options) {
ts.Debug.assert(ts.contains(allKeys, key));
if (!ts.contains(allKeys, key)) {
ts.Debug.fail("Unexpected key " + key);
Copy link
Member

Choose a reason for hiding this comment

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

You should return ts.Debug.fail("Unexpected key " + key);, probably.

Copy link
Member Author

@DanielRosenwasser DanielRosenwasser Jul 9, 2018

Choose a reason for hiding this comment

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

I usually only do that when the compiler needs to know more about reachability, but I don't think it's necessary here unless I'm missing something.

Copy link
Member

Choose a reason for hiding this comment

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

We were talking about making it a lint rule a few weeks ago, that way the reachability graph is more complete and may expose more logic bugs (eg, can a following if statement never actually occur because of the implied throw here).

}
}
}

Expand Down
66 changes: 55 additions & 11 deletions src/services/signatureHelp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ namespace ts.SignatureHelp {
return undefined;
}

if (shouldCarefullyCheckContext(triggerReason)) {
// In the middle of a string, don't provide signature help unless the user explicitly requested it.
if (isInString(sourceFile, position, startingToken)) {
// Only need to be careful if the user typed a character and signature help wasn't showing.
const shouldCarefullyCheckContext = !!triggerReason && triggerReason.kind === "characterTyped";

// Bail out quickly in the middle of a string or comment, don't provide signature help unless the user explicitly requested it.
if (shouldCarefullyCheckContext) {
if (isInString(sourceFile, position, startingToken) || isInComment(sourceFile, position)) {
return undefined;
}
}
Expand All @@ -41,8 +44,8 @@ namespace ts.SignatureHelp {

cancellationToken.throwIfCancellationRequested();

// Semantic filtering of signature help
const candidateInfo = getCandidateInfo(argumentInfo, typeChecker);
// Extra syntactic and semantic filtering of signature help
const candidateInfo = getCandidateInfo(argumentInfo, typeChecker, sourceFile, startingToken, shouldCarefullyCheckContext);
cancellationToken.throwIfCancellationRequested();

if (!candidateInfo) {
Expand All @@ -57,24 +60,57 @@ namespace ts.SignatureHelp {
return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker));
}

function shouldCarefullyCheckContext(reason: SignatureHelpTriggerReason | undefined) {
// Only need to be careful if the user typed a character and signature help wasn't showing.
return !!reason && reason.kind === "characterTyped";
}
function getCandidateInfo(
argumentInfo: ArgumentListInfo, checker: TypeChecker, sourceFile: SourceFile, startingToken: Node, onlyUseSyntacticOwners: boolean):
{ readonly candidates: ReadonlyArray<Signature>, readonly resolvedSignature: Signature } | undefined {

function getCandidateInfo(argumentInfo: ArgumentListInfo, checker: TypeChecker): { readonly candidates: ReadonlyArray<Signature>, readonly resolvedSignature: Signature } | undefined {
const { invocation } = argumentInfo;
if (invocation.kind === InvocationKind.Call) {
if (onlyUseSyntacticOwners) {
if (isCallOrNewExpression(invocation.node)) {
const invocationChildren = invocation.node.getChildren(sourceFile);
switch (startingToken.kind) {
case SyntaxKind.OpenParenToken:
if (!contains(invocationChildren, startingToken)) {
return undefined;
}
break;
case SyntaxKind.CommaToken:
const containingList = findContainingList(startingToken);
if (!containingList || !contains(invocationChildren, findContainingList(startingToken))) {
return undefined;
}
break;
case SyntaxKind.LessThanToken:
if (!lessThanFollowsCalledExpression(startingToken, sourceFile, invocation.node.expression)) {
return undefined;
}
break;
default:
return undefined;
}
}
else {
return undefined;
}
}

const candidates: Signature[] = [];
const resolvedSignature = checker.getResolvedSignature(invocation.node, candidates, argumentInfo.argumentCount)!; // TODO: GH#18217
return candidates.length === 0 ? undefined : { candidates, resolvedSignature };
}
else {
else if (invocation.kind === InvocationKind.TypeArgs) {
if (onlyUseSyntacticOwners && !lessThanFollowsCalledExpression(startingToken, sourceFile, invocation.called)) {
return undefined;
}
const type = checker.getTypeAtLocation(invocation.called)!; // TODO: GH#18217
const signatures = isNewExpression(invocation.called.parent) ? type.getConstructSignatures() : type.getCallSignatures();
const candidates = signatures.filter(candidate => !!candidate.typeParameters && candidate.typeParameters.length >= argumentInfo.argumentCount);
return candidates.length === 0 ? undefined : { candidates, resolvedSignature: first(candidates) };
}
else {
Debug.assertNever(invocation);
}
}

function createJavaScriptSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program, cancellationToken: CancellationToken): SignatureHelpItems | undefined {
Expand Down Expand Up @@ -107,6 +143,14 @@ namespace ts.SignatureHelp {
}
}

function lessThanFollowsCalledExpression(startingToken: Node, sourceFile: SourceFile, calledExpression: Expression) {
const precedingToken = Debug.assertDefined(
findPrecedingToken(startingToken.getFullStart(), sourceFile, startingToken.parent, /*excludeJsdoc*/ true)
);

return rangeContainsRange(calledExpression, precedingToken);
}

export interface ArgumentInfoForCompletions {
readonly invocation: CallLikeExpression;
readonly argumentIndex: number;
Expand Down
1 change: 0 additions & 1 deletion tests/cases/fourslash/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,6 @@ declare namespace FourSlashInterface {
overloadsCount?: number;
docComment?: string;
text?: string;
name?: string;
parameterName?: string;
parameterSpan?: string;
parameterDocComment?: string;
Expand Down
23 changes: 0 additions & 23 deletions tests/cases/fourslash/signatureHelpFilteredTriggerCharacters01.ts

This file was deleted.

31 changes: 31 additions & 0 deletions tests/cases/fourslash/signatureHelpFilteredTriggers01.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// <reference path="fourslash.ts" />

////function foo<T>(x: T): T {
//// throw null;
////}
////
////foo("/*1*/");
////foo('/*2*/');
////foo(` ${100}/*3*/`);
////foo(/* /*4*/ */);
////foo(
//// ///*5*/
////);

for (const marker of test.markers()) {
goTo.marker(marker);
for (const triggerCharacter of ["<", "(", ","]) {
edit.insert(triggerCharacter);
verify.noSignatureHelpForTriggerReason({
kind: "characterTyped",
triggerCharacter,
});
verify.signatureHelpPresentForTriggerReason({
kind: "retrigger",
triggerCharacter,
});
edit.backspace();
}
verify.signatureHelpPresentForTriggerReason(/*triggerReason*/ undefined);
verify.signatureHelpPresentForTriggerReason({ kind: "invoked" });
}
36 changes: 36 additions & 0 deletions tests/cases/fourslash/signatureHelpFilteredTriggers02.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/// <reference path="fourslash.ts" />

////function foo<T>(x: T): T {
//// throw null;
////}
////
////foo(/*1*/"");
////foo(` ${100/*2*/}`);
////foo(/*3*/);
////foo(100 /*4*/)
////foo([/*5*/])
////foo({ hello: "hello"/*6*/})

const charMap = {
1: "(",
2: ",",
3: "(",
4: "<",
5: ",",
6: ",",
}

for (const markerName of Object.keys(charMap)) {
const triggerCharacter = charMap[markerName];
goTo.marker(markerName);
edit.insert(triggerCharacter);
verify.noSignatureHelpForTriggerReason({
kind: "characterTyped",
triggerCharacter,
});
verify.signatureHelpPresentForTriggerReason({
kind: "retrigger",
triggerCharacter,
});
edit.backspace(triggerCharacter.length);
}
23 changes: 23 additions & 0 deletions tests/cases/fourslash/signatureHelpFilteredTriggers03.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <reference path="fourslash.ts" />

////declare class ViewJayEss {
//// constructor(obj: object);
////}
////new ViewJayEss({
//// methods: {
//// sayHello/**/
//// }
////});

goTo.marker();
edit.insert("(");
verify.noSignatureHelpForTriggerReason({
kind: "characterTyped",
triggerCharacter: "(",
});

edit.insert(") {},");
verify.noSignatureHelpForTriggerReason({
kind: "characterTyped",
triggerCharacter: ",",
});
31 changes: 31 additions & 0 deletions tests/cases/fourslash/signatureHelpWithTriggers01.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// <reference path="fourslash.ts" />

////declare function foo<T>(x: T, y: T): T;
////
////foo/*1*//*2*/;
////foo(/*3*/100/*4*/);
////foo/*5*//*6*/();

const charMap = {
1: "(",
2: "<",
3: ",",
4: ",",
5: "(",
6: "<",
}

for (const markerName of Object.keys(charMap)) {
const triggerCharacter = charMap[markerName];
goTo.marker(markerName);
edit.insert(triggerCharacter);
verify.signatureHelpPresentForTriggerReason({
kind: "characterTyped",
triggerCharacter,
});
verify.signatureHelpPresentForTriggerReason({
kind: "retrigger",
triggerCharacter,
});
edit.backspace(triggerCharacter.length);
}
38 changes: 38 additions & 0 deletions tests/cases/fourslash/signatureHelpWithTriggers02.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/// <reference path="fourslash.ts" />

////declare function foo<T>(x: T, y: T): T;
////declare function bar<U>(x: U, y: U): U;
////
////foo(bar/*1*/)

goTo.marker("1");

edit.insert("(");
verify.signatureHelp({
text: "bar<U>(x: U, y: U): U",
triggerReason: {
kind: "characterTyped",
triggerCharacter: "(",
}
});
edit.backspace();

edit.insert("<");
verify.signatureHelp({
text: "bar<U>(x: U, y: U): U",
triggerReason: {
kind: "characterTyped",
triggerCharacter: "(",
}
});
edit.backspace();

edit.insert(",");
verify.signatureHelp({
text: "foo(x: <U>(x: U, y: U) => U, y: <U>(x: U, y: U) => U): <U>(x: U, y: U) => U",
triggerReason: {
kind: "characterTyped",
triggerCharacter: "(",
}
});
edit.backspace();