Skip to content

Commit a37a257

Browse files
Copilotjakebailey
andauthored
Fix completion preselection in ternary conditional expressions (#2211)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: jakebailey <[email protected]>
1 parent 8beb55c commit a37a257

File tree

2 files changed

+134
-0
lines changed

2 files changed

+134
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/fourslash"
7+
. "github.com/microsoft/typescript-go/internal/fourslash/tests/util"
8+
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
9+
"github.com/microsoft/typescript-go/internal/testutil"
10+
)
11+
12+
func TestCompletionInTernaryConditional(t *testing.T) {
13+
t.Parallel()
14+
15+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
16+
const content = `export enum Bar { }
17+
export enum Foo { }
18+
19+
20+
function foo(x: Foo) { return x; }
21+
function bar(z: string, x: Foo) { return x; }
22+
23+
const a = '';
24+
25+
foo(/*1*/);
26+
bar(a, a == '' ? /*2*/);
27+
bar(a, a == '' ? /*3*/ : /*4*/);`
28+
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
29+
defer done()
30+
31+
// Test marker 1 - should have Foo preselected in simple call
32+
f.VerifyCompletions(t, "1", &fourslash.CompletionsExpectedList{
33+
IsIncomplete: false,
34+
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
35+
CommitCharacters: &DefaultCommitCharacters,
36+
EditRange: Ignored,
37+
},
38+
Items: &fourslash.CompletionsExpectedItems{
39+
Includes: []fourslash.CompletionsExpectedItem{
40+
&lsproto.CompletionItem{
41+
Label: "Foo",
42+
Kind: PtrTo(lsproto.CompletionItemKindEnum),
43+
Preselect: PtrTo(true),
44+
},
45+
},
46+
},
47+
})
48+
49+
// Test marker 2 - should have Foo preselected after ? in incomplete ternary
50+
f.VerifyCompletions(t, "2", &fourslash.CompletionsExpectedList{
51+
IsIncomplete: false,
52+
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
53+
CommitCharacters: &DefaultCommitCharacters,
54+
EditRange: Ignored,
55+
},
56+
Items: &fourslash.CompletionsExpectedItems{
57+
Includes: []fourslash.CompletionsExpectedItem{
58+
&lsproto.CompletionItem{
59+
Label: "Foo",
60+
Kind: PtrTo(lsproto.CompletionItemKindEnum),
61+
Preselect: PtrTo(true),
62+
},
63+
},
64+
},
65+
})
66+
67+
// Test marker 3 - should have Foo preselected after ? in ternary with colon
68+
f.VerifyCompletions(t, "3", &fourslash.CompletionsExpectedList{
69+
IsIncomplete: false,
70+
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
71+
CommitCharacters: &DefaultCommitCharacters,
72+
EditRange: Ignored,
73+
},
74+
Items: &fourslash.CompletionsExpectedItems{
75+
Includes: []fourslash.CompletionsExpectedItem{
76+
&lsproto.CompletionItem{
77+
Label: "Foo",
78+
Kind: PtrTo(lsproto.CompletionItemKindEnum),
79+
Preselect: PtrTo(true),
80+
},
81+
},
82+
},
83+
})
84+
85+
// Test marker 4 - should have Foo preselected after : in ternary
86+
f.VerifyCompletions(t, "4", &fourslash.CompletionsExpectedList{
87+
IsIncomplete: false,
88+
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
89+
CommitCharacters: &DefaultCommitCharacters,
90+
EditRange: Ignored,
91+
},
92+
Items: &fourslash.CompletionsExpectedItems{
93+
Includes: []fourslash.CompletionsExpectedItem{
94+
&lsproto.CompletionItem{
95+
Label: "Foo",
96+
Kind: PtrTo(lsproto.CompletionItemKindEnum),
97+
Preselect: PtrTo(true),
98+
},
99+
},
100+
},
101+
})
102+
}

internal/ls/completions.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2923,6 +2923,21 @@ func isStaticProperty(symbol *ast.Symbol) bool {
29232923
ast.IsClassLike(symbol.ValueDeclaration.Parent)
29242924
}
29252925

2926+
// getContextualTypeForConditionalExpression handles completion within a conditional expression
2927+
// (ternary operator) by using the parent expression to find the contextual type.
2928+
func getContextualTypeForConditionalExpression(conditionalExpr *ast.Node, position int, file *ast.SourceFile, typeChecker *checker.Checker) *checker.Type {
2929+
argInfo := getArgumentInfoForCompletions(conditionalExpr, position, file, typeChecker)
2930+
if argInfo != nil {
2931+
return typeChecker.GetContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex)
2932+
}
2933+
// Fall through to regular contextual type logic if not in an argument
2934+
contextualType := typeChecker.GetContextualType(conditionalExpr, checker.ContextFlagsCompletions)
2935+
if contextualType != nil {
2936+
return contextualType
2937+
}
2938+
return typeChecker.GetContextualType(conditionalExpr, checker.ContextFlagsNone)
2939+
}
2940+
29262941
func getContextualType(previousToken *ast.Node, position int, file *ast.SourceFile, typeChecker *checker.Checker) *checker.Type {
29272942
parent := previousToken.Parent
29282943
switch previousToken.Kind {
@@ -2952,6 +2967,23 @@ func getContextualType(previousToken *ast.Node, position int, file *ast.SourceFi
29522967
return typeChecker.GetContextualTypeForJsxAttribute(parent.Parent)
29532968
}
29542969
return nil
2970+
case ast.KindQuestionToken:
2971+
// When completing after `?` in a ternary conditional (e.g., `foo(a ? /*here*/)`),
2972+
// we need to look at the parent conditional expression to find the contextual type.
2973+
if ast.IsConditionalExpression(parent) {
2974+
return getContextualTypeForConditionalExpression(parent, position, file, typeChecker)
2975+
}
2976+
return nil
2977+
case ast.KindColonToken:
2978+
// When completing after `:` in a ternary conditional (e.g., `foo(a ? b : /*here*/)`),
2979+
// we need to look at the parent conditional expression to find the contextual type.
2980+
// Only handle this if parent is ConditionalExpression, otherwise fall through to default
2981+
// (colons are used in other contexts like object literals, type annotations, etc.)
2982+
if ast.IsConditionalExpression(parent) {
2983+
return getContextualTypeForConditionalExpression(parent, position, file, typeChecker)
2984+
}
2985+
// Fall through to default for other colon contexts (object literals, etc.)
2986+
fallthrough
29552987
default:
29562988
argInfo := getArgumentInfoForCompletions(previousToken, position, file, typeChecker)
29572989
if argInfo != nil {

0 commit comments

Comments
 (0)