diff --git a/internal/fourslash/tests/signatureHelpApplicableRange_test.go b/internal/fourslash/tests/signatureHelpApplicableRange_test.go new file mode 100644 index 0000000000..22b3998f5c --- /dev/null +++ b/internal/fourslash/tests/signatureHelpApplicableRange_test.go @@ -0,0 +1,46 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +// Test case 1: Basic applicable range test +// This test verifies the applicable range for signature help +// According to the issue, signature help should be provided: +// - Inside the parentheses (markers 1, 2) +// - NOT on the call target before the opening paren (marker a, b, c would test this) +// - NOT after the closing paren including whitespace (markers 3, 4) +func TestSignatureHelpApplicableRangeBasic(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `let obj = { + foo(s: string): string { + return s; + } +}; + +let s =/*a*/ /*b*/obj/*c*/./*d*/foo/*e*/(/*1*/"Hello, world!"/*2*/)/*3*/ + /*4*/;` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + + // Use VerifyBaselineSignatureHelp to check behavior at all markers + f.VerifyBaselineSignatureHelp(t) +} + +// Test case 2: Nested calls - outer should take precedence +func TestSignatureHelpNestedCalls(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `function foo(s: string) { return s; } +function bar(s: string) { return s; } +let s = foo(/*a*//*b*/bar/*c*/(/*d*/"hello"/*e*/)/*f*/);` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + + // Use VerifyBaselineSignatureHelp to check behavior at all markers + f.VerifyBaselineSignatureHelp(t) +} diff --git a/internal/ls/signaturehelp.go b/internal/ls/signaturehelp.go index b1d33260c8..e02752d35b 100644 --- a/internal/ls/signaturehelp.go +++ b/internal/ls/signaturehelp.go @@ -614,16 +614,37 @@ func containsPrecedingToken(startingToken *ast.Node, sourceFile *ast.SourceFile, } func getContainingArgumentInfo(node *ast.Node, sourceFile *ast.SourceFile, checker *checker.Checker, isManuallyInvoked bool, position int) *argumentListInfo { + var lastArgumentInfo *argumentListInfo for n := node; !ast.IsSourceFile(n) && (isManuallyInvoked || !ast.IsBlock(n)); n = n.Parent { // If the node is not a subspan of its parent, this is a big problem. // There have been crashes that might be caused by this violation. debug.Assert(RangeContainsRange(n.Parent.Loc, n.Loc), fmt.Sprintf("Not a subspan. Child: %s, parent: %s", n.KindString(), n.Parent.KindString())) argumentInfo := getImmediatelyContainingArgumentOrContextualParameterInfo(n, position, sourceFile, checker) if argumentInfo != nil { - return argumentInfo + // For contextual invocations (e.g., arrow functions with contextual types), + // always return immediately without checking the position. + // This ensures that when inside a callback's parameter list, we show the callback's + // signature, not the outer call's signature. + if argumentInfo.invocation.contextualInvocation != nil { + return argumentInfo + } + + // For regular call expressions, check if the position is actually within the applicable span. + // This ensures that for nested calls, the outer call takes precedence + // when the position is outside the inner call's argument list. + if argumentInfo.argumentsSpan.Contains(position) { + return argumentInfo + } + // Remember this argument info in case we don't find an outer call + if lastArgumentInfo == nil { + lastArgumentInfo = argumentInfo + } + // Continue looking for an outer call if position is outside this call's applicable span } } - return nil + // If we didn't find a call that contains the position, return the last call we found. + // This handles cases where the cursor is at the edge of a call (e.g., right after a parameter). + return lastArgumentInfo } func getImmediatelyContainingArgumentOrContextualParameterInfo(node *ast.Node, position int, sourceFile *ast.SourceFile, checker *checker.Checker) *argumentListInfo { @@ -895,14 +916,15 @@ func getArgumentOrParameterListInfo(node *ast.Node, sourceFile *ast.SourceFile, } func getApplicableSpanForArguments(argumentList *ast.NodeList, node *ast.Node, sourceFile *ast.SourceFile) core.TextRange { - // We use full start and skip trivia on the end because we want to include trivia on - // both sides. For example, + // The applicable span starts at the beginning of the argument list (including leading trivia) + // and extends to the end of the argument list plus any trailing trivia. + // 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) + // but not including parentheses). if argumentList == nil && node != nil { // If the user has just opened a list, and there are no arguments. // For example, foo( ) diff --git a/testdata/baselines/reference/fourslash/signatureHelp/signatureHelpApplicableRangeBasic.baseline b/testdata/baselines/reference/fourslash/signatureHelp/signatureHelpApplicableRangeBasic.baseline new file mode 100644 index 0000000000..f31be9c084 --- /dev/null +++ b/testdata/baselines/reference/fourslash/signatureHelp/signatureHelpApplicableRangeBasic.baseline @@ -0,0 +1,182 @@ +// === SignatureHelp === +=== /signatureHelpApplicableRangeBasic.ts === +// let obj = { +// foo(s: string): string { +// return s; +// } +// }; +// +// let s = obj.foo("Hello, world!") +// ^ +// | ---------------------------------------------------------------------- +// | No signaturehelp at /*a*/. +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | No signaturehelp at /*b*/. +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | No signaturehelp at /*c*/. +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | No signaturehelp at /*d*/. +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | No signaturehelp at /*e*/. +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | foo(**s: string**): string +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | foo(**s: string**): string +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | No signaturehelp at /*3*/. +// | ---------------------------------------------------------------------- +// ; +// ^ +// | ---------------------------------------------------------------------- +// | No signaturehelp at /*4*/. +// | ---------------------------------------------------------------------- +[ + { + "marker": { + "Position": 76, + "LSPosition": { + "line": 6, + "character": 7 + }, + "Name": "a", + "Data": {} + }, + "item": null + }, + { + "marker": { + "Position": 77, + "LSPosition": { + "line": 6, + "character": 8 + }, + "Name": "b", + "Data": {} + }, + "item": null + }, + { + "marker": { + "Position": 80, + "LSPosition": { + "line": 6, + "character": 11 + }, + "Name": "c", + "Data": {} + }, + "item": null + }, + { + "marker": { + "Position": 81, + "LSPosition": { + "line": 6, + "character": 12 + }, + "Name": "d", + "Data": {} + }, + "item": null + }, + { + "marker": { + "Position": 84, + "LSPosition": { + "line": 6, + "character": 15 + }, + "Name": "e", + "Data": {} + }, + "item": null + }, + { + "marker": { + "Position": 85, + "LSPosition": { + "line": 6, + "character": 16 + }, + "Name": "1", + "Data": {} + }, + "item": { + "signatures": [ + { + "label": "foo(s: string): string", + "parameters": [ + { + "label": "s: string" + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 0 + } + }, + { + "marker": { + "Position": 100, + "LSPosition": { + "line": 6, + "character": 31 + }, + "Name": "2", + "Data": {} + }, + "item": { + "signatures": [ + { + "label": "foo(s: string): string", + "parameters": [ + { + "label": "s: string" + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 0 + } + }, + { + "marker": { + "Position": 101, + "LSPosition": { + "line": 6, + "character": 32 + }, + "Name": "3", + "Data": {} + }, + "item": null + }, + { + "marker": { + "Position": 106, + "LSPosition": { + "line": 7, + "character": 2 + }, + "Name": "4", + "Data": {} + }, + "item": null + } +] \ No newline at end of file diff --git a/testdata/baselines/reference/fourslash/signatureHelp/signatureHelpNestedCalls.baseline b/testdata/baselines/reference/fourslash/signatureHelp/signatureHelpNestedCalls.baseline new file mode 100644 index 0000000000..4788c33924 --- /dev/null +++ b/testdata/baselines/reference/fourslash/signatureHelp/signatureHelpNestedCalls.baseline @@ -0,0 +1,181 @@ +// === SignatureHelp === +=== /signatureHelpNestedCalls.ts === +// function foo(s: string) { return s; } +// function bar(s: string) { return s; } +// let s = foo(bar("hello")); +// ^ +// | ---------------------------------------------------------------------- +// | foo(**s: string**): string +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | foo(**s: string**): string +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | foo(**s: string**): string +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | bar(**s: string**): string +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | foo(**s: string**): string +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | foo(**s: string**): string +// | ---------------------------------------------------------------------- +[ + { + "marker": { + "Position": 88, + "LSPosition": { + "line": 2, + "character": 12 + }, + "Name": "a", + "Data": {} + }, + "item": { + "signatures": [ + { + "label": "foo(s: string): string", + "parameters": [ + { + "label": "s: string" + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 0 + } + }, + { + "marker": { + "Position": 88, + "LSPosition": { + "line": 2, + "character": 12 + }, + "Name": "b", + "Data": {} + }, + "item": { + "signatures": [ + { + "label": "foo(s: string): string", + "parameters": [ + { + "label": "s: string" + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 0 + } + }, + { + "marker": { + "Position": 91, + "LSPosition": { + "line": 2, + "character": 15 + }, + "Name": "c", + "Data": {} + }, + "item": { + "signatures": [ + { + "label": "foo(s: string): string", + "parameters": [ + { + "label": "s: string" + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 0 + } + }, + { + "marker": { + "Position": 92, + "LSPosition": { + "line": 2, + "character": 16 + }, + "Name": "d", + "Data": {} + }, + "item": { + "signatures": [ + { + "label": "bar(s: string): string", + "parameters": [ + { + "label": "s: string" + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 0 + } + }, + { + "marker": { + "Position": 99, + "LSPosition": { + "line": 2, + "character": 23 + }, + "Name": "e", + "Data": {} + }, + "item": { + "signatures": [ + { + "label": "foo(s: string): string", + "parameters": [ + { + "label": "s: string" + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 0 + } + }, + { + "marker": { + "Position": 100, + "LSPosition": { + "line": 2, + "character": 24 + }, + "Name": "f", + "Data": {} + }, + "item": { + "signatures": [ + { + "label": "foo(s: string): string", + "parameters": [ + { + "label": "s: string" + } + ] + } + ], + "activeSignature": 0, + "activeParameter": 0 + } + } +] \ No newline at end of file