Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 24, 2025

Problem

Signature help had incorrect behavior for nested function calls. When the cursor was positioned after completing an inner call but still within the outer call's arguments, signature help would show the inner call's signature instead of the outer call's signature.

For example, in this code:

function foo(s: string) { return s; }
function bar(s: string) { return s; }
let s = foo(bar("hello"));
//              cursor positions:
//          a b    c    d      e  f

The expected behavior is:

  • Positions a, b, c, e, f: Show signature for foo (outer call takes precedence)
  • Position d: Show signature for bar (inside inner call's arguments)

However, the implementation was showing bar's signature at position e (after "hello"), when it should show foo's signature since the user has completed typing the inner call and needs to know what the outer call expects.

Solution

Added position validation in getContainingArgumentInfo to check if the cursor is actually within a call's applicable argument span before returning its signature help. When the cursor is outside an inner call's argument list, the algorithm now continues searching up the tree to find an outer call that contains the position.

The fix includes:

  1. Position checking: Validates that the cursor position is within the applicable span using argumentsSpan.Contains(position) before returning signature help for a call
  2. Contextual invocation handling: Preserves correct behavior for callbacks and arrow functions with contextual types by always returning their signatures when inside their parameter lists
  3. Fallback logic: Returns the last found call if no outer call contains the position, handling edge cases like cursors right after parameters

Test Coverage

Added two comprehensive test cases:

  • TestSignatureHelpApplicableRangeBasic: Validates signature help is not provided outside call boundaries
  • TestSignatureHelpNestedCalls: Validates correct precedence for all positions in the nested call scenario described above

All 23 existing signature help tests continue to pass, confirming no regressions were introduced.

Example

// Before this fix:
foo(bar("hello"))
//             ^ shows bar's signature (wrong)

// After this fix:
foo(bar("hello"))
//             ^ shows foo's signature (correct - outer call takes precedence)

Fixes the nested call precedence issues described in the original issue while maintaining backward compatibility with existing signature help behavior.

Original prompt

This section details on the original issue you should resolve

<issue_title>Signature help is wrong for nested calls, has weird applicable ranges</issue_title>
<issue_description>Here's an example file that exhibits new weirdness on signature help:

let obj = {
    foo(s: string): string {
        return s;
    }
};

let s =[| obj.foo("Hello, world!")  
  |];

Note that the spaces/newlines after [| and before |]` are intentional for the point I'm making.

If you request signature help at every position within the range, you will get different results compared to the old language service. Previously, signature help for foo would only be provided inside of the parentheses.

In the native port, signature help is now provided on parts of the call target, along with all whitespace following the parentheses.

Specifically, the actual positions you will get signature help on only fall within the following range:

let s = o[|bj.foo("Hello, world!")  
  |];
signature-help-weird-1.webm

I think that it is fine for us to provide signature help on a call target like obj.foo, but it is not including the correct start.

We should not include the whitespace after the call at all.

In other words someone should start with this test case:

let obj = {
    foo(s: string): string {
        return s;
    }
};

let s =/*a*/ [|obj.foo("Hello, world!"|])/*b*/  
  /*c*/;

Given the above test, signature help should be provided for every position in the range, but not for markers a, b, or c.

This is similar to what the Go LS does in that the position after the ) is not included in signature help. On the other hand, it does differ in that we would return signature help on all of obj.foo (Go only provides signature help on foo).


Speaking of which, we should have another test case on this as well:

function someFunc() {
    return {
        foo() {
            return "hello!"
        }
    }
}

[|someFunc(|][|).foo(|][|).toUpperCase(|]);

The test should verify that signature help for all positions:

  • Range 1: Label = someFunc
  • Range 2: Label = foo
  • Range 3: Label = toUpperCase

With all of that considered, there's a regression in signature help where inner calls take precedence over outer calls

signature-help-weird-2.webm
function foo(x: any) {
    return x;
}

function bar(x: any) {
    return x;
}

foo(/*a*/ /*b*/bar/*c*/(/*d*/)/*e*/ /*f*/)

In my opinion, signature help should work in the following way:

  • Markers a, b, c, e, f: Label = foo
  • Marker d: Label = bar

In other words, if there is an outer call, I think it takes precedence over signature help from the call target.

The Go LS actually prioritizes the call target rather than the outer call, and I actually don't think that behavior is usually desirable. Doing so means you need to move your cursor to the end of the call or add a space before the call target.</issue_description>

<agent_instructions>Reread Daniel's comments on #1420 too, but note that the PR itself is likely not correct.</agent_instructions>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 4 commits October 24, 2025 17:54
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
- Remove scanner.SkipTrivia from getApplicableSpanForArguments to avoid including whitespace after arguments
- Add position check in getContainingArgumentInfo to ensure outer calls take precedence over inner calls
- Most nested call cases now work correctly (markers a, b, c, d, e)
- Known issue: marker f (at the end of nested call) still returns null instead of showing outer call
- This represents significant progress on the issue but needs further refinement

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
- Add position check in getContainingArgumentInfo to ensure outer calls take precedence when position is outside inner call's argument list
- Exclude contextual invocations from position check to preserve correct behavior for callbacks
- Add fallback logic to return the last found call if no outer call contains the position
- All signature help tests now pass
- Nested call test shows correct behavior: outer call takes precedence except when inside inner call's arguments

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
- Remove trailing whitespace
- Clarify comment in getApplicableSpanForArguments
- Accept baselines for new test cases

Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix signature help for nested calls and applicable ranges Fix signature help to prioritize outer calls over inner calls in nested invocations Oct 24, 2025
Copilot AI requested a review from jakebailey October 24, 2025 18:33
Copilot finished work on behalf of jakebailey October 24, 2025 18:33
Comment on lines +645 to +647
// 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
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why we need this.

@copilot is there a test file that shows where this helps? What happens if you remove it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The fallback logic (lines 645-647) is needed to handle edge cases where the cursor is inside a call's parentheses but outside the computed applicable span. This happens in several scenarios:

  1. Empty argument lists: foo(/*cursor*/) - the cursor is inside the parentheses but there are no arguments, so the applicable span may not include this position
  2. After the last argument: foo(arg/*cursor*/) - when trivia/whitespace follows the last argument

Without this fallback, 14 existing tests fail (e.g., TestSignatureHelpJSDocTags where markers like Foo.method1(/*11*/) return null instead of showing signature help).

The fallback ensures that if we find a call while walking up the tree but the position is slightly outside its span (and there's no outer call), we still show that call's signature help rather than returning null. This preserves the existing behavior for non-nested calls while still fixing the nested call precedence issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Signature help is wrong for nested calls, has weird applicable ranges

3 participants