Skip to content
Draft
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
26 changes: 26 additions & 0 deletions internal/fourslash/tests/signatureHelpApplicableRange_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestSignatureHelpApplicableRange(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*/ obj.foo("Hello, world!")/*b*/
/*c*/;`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)

// Markers a, b, c should NOT show signature help (outside the call)
f.VerifyNoSignatureHelpForMarkers(t, "a", "b", "c")
}
39 changes: 39 additions & 0 deletions internal/fourslash/tests/signatureHelpNestedCalls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

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)

// Markers a, b, c should show foo (outer call)
f.GoToMarker(t, "a")
f.VerifySignatureHelp(t, fourslash.VerifySignatureHelpOptions{Text: "foo(s: string): string"})

f.GoToMarker(t, "b")
f.VerifySignatureHelp(t, fourslash.VerifySignatureHelpOptions{Text: "foo(s: string): string"})

f.GoToMarker(t, "c")
f.VerifySignatureHelp(t, fourslash.VerifySignatureHelpOptions{Text: "foo(s: string): string"})

// Marker d should show bar (inside inner call)
f.GoToMarker(t, "d")
f.VerifySignatureHelp(t, fourslash.VerifySignatureHelpOptions{Text: "bar(s: string): string"})

// Markers e, f should show foo (after inner call)
f.GoToMarker(t, "e")
f.VerifySignatureHelp(t, fourslash.VerifySignatureHelpOptions{Text: "foo(s: string): string"})

f.GoToMarker(t, "f")
f.VerifySignatureHelp(t, fourslash.VerifySignatureHelpOptions{Text: "foo(s: string): string"})
}
55 changes: 49 additions & 6 deletions internal/ls/signaturehelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -750,16 +750,38 @@ 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 firstArgumentInfo *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
}

// Remember the first (innermost) argument info we find
if firstArgumentInfo == nil {
firstArgumentInfo = argumentInfo
}

// If any call's span contains the position, return it.
// We walk from inner to outer, so this naturally prefers the innermost call
// when multiple calls contain the position.
if argumentInfo.argumentsSpan.Contains(position) {
return argumentInfo
}
}
}
return nil

// No call's span contains the position. Return the innermost call as fallback.
// This handles cases like foo(bar(|)) where bar's span might be empty.
return firstArgumentInfo
}

func getImmediatelyContainingArgumentOrContextualParameterInfo(node *ast.Node, position int, sourceFile *ast.SourceFile, checker *checker.Checker) *argumentListInfo {
Expand Down Expand Up @@ -1031,25 +1053,46 @@ 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( )
// | |
return core.NewTextRange(node.End(), scanner.SkipTrivia(sourceFile.Text(), node.End()))
// The span should include positions inside the parentheses.
spanStart := node.End()
spanEnd := scanner.SkipTrivia(sourceFile.Text(), node.End())
// Ensure the span includes at least the position right after the opening paren
spanEnd = ensureMinimumSpanSize(spanStart, spanEnd)
return core.NewTextRange(spanStart, spanEnd)
}
applicableSpanStart := argumentList.Pos()
applicableSpanEnd := scanner.SkipTrivia(sourceFile.Text(), argumentList.End())

// If the argument list is empty (Pos == End), extend the span to include at least
// one position. This handles foo(|) where the cursor is right after the opening paren.
applicableSpanEnd = ensureMinimumSpanSize(applicableSpanStart, applicableSpanEnd)

return core.NewTextRange(applicableSpanStart, applicableSpanEnd)
}

// ensureMinimumSpanSize ensures that a span includes at least one position.
// This is necessary for empty argument lists where start == end would create
// a span that doesn't contain any position.
func ensureMinimumSpanSize(start, end int) int {
if end <= start {
return start + 1
}
return end
}

type argumentOrParameterListAndIndex struct {
list *ast.NodeList
argumentIndex int
Expand Down
Loading