Skip to content

Commit 6cd5949

Browse files
authored
String in string completions + plain switch completion (#893)
1 parent 0a11b9f commit 6cd5949

File tree

12 files changed

+1407
-380
lines changed

12 files changed

+1407
-380
lines changed

internal/ast/ast.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,8 @@ type (
17961796
ObjectLiteralExpressionNode = Node
17971797
ConstructorDeclarationNode = Node
17981798
NamedExportsNode = Node
1799+
UnionType = Node
1800+
LiteralType = Node
17991801
)
18001802

18011803
type (

internal/ast/utilities.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2952,3 +2952,15 @@ func GetPropertyNameForPropertyNameNode(name *Node) string {
29522952
}
29532953
panic("Unhandled case in getPropertyNameForPropertyNameNode")
29542954
}
2955+
2956+
func IsStringTextContainingNode(node *Node) bool {
2957+
return node.Kind == KindStringLiteral || IsTemplateLiteralKind(node.Kind)
2958+
}
2959+
2960+
func IsTemplateLiteralKind(kind Kind) bool {
2961+
return KindFirstTemplateToken <= kind && kind <= KindLastTemplateToken
2962+
}
2963+
2964+
func IsTemplateLiteralToken(node *Node) bool {
2965+
return IsTemplateLiteralKind(node.Kind)
2966+
}

internal/checker/exports.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,7 @@ func GetDeclarationModifierFlagsFromSymbol(s *ast.Symbol) ast.ModifierFlags {
5252
func (c *Checker) WasCanceled() bool {
5353
return c.wasCanceled
5454
}
55+
56+
func (c *Checker) GetBaseConstraintOfType(t *Type) *Type {
57+
return c.getBaseConstraintOfType(t)
58+
}

internal/checker/services.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,31 @@ func (c *Checker) GetJsxIntrinsicTagNamesAt(location *ast.Node) []*ast.Symbol {
482482
func (c *Checker) GetContextualTypeForJsxAttribute(attribute *ast.JsxAttributeLike) *Type {
483483
return c.getContextualTypeForJsxAttribute(attribute, ContextFlagsNone)
484484
}
485+
486+
func (c *Checker) GetConstantValue(node *ast.Node) any {
487+
if node.Kind == ast.KindEnumMember {
488+
return c.getEnumMemberValue(node).Value
489+
}
490+
491+
if c.symbolNodeLinks.Get(node).resolvedSymbol == nil {
492+
c.checkExpressionCached(node) // ensure cached resolved symbol is set
493+
}
494+
symbol := c.symbolNodeLinks.Get(node).resolvedSymbol
495+
if symbol == nil && ast.IsEntityNameExpression(node) {
496+
symbol = c.resolveEntityName(
497+
node,
498+
ast.SymbolFlagsValue,
499+
true, /*ignoreErrors*/
500+
false, /*dontResolveAlias*/
501+
nil /*location*/)
502+
}
503+
if symbol != nil && symbol.Flags&ast.SymbolFlagsEnumMember != 0 {
504+
// inline property\index accesses only for const enums
505+
member := symbol.ValueDeclaration
506+
if ast.IsEnumConst(member.Parent) {
507+
return c.getEnumMemberValue(member).Value
508+
}
509+
}
510+
511+
return nil
512+
}

internal/checker/types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,10 @@ func (t *Type) IsClass() bool {
680680
return t.objectFlags&ObjectFlagsClass != 0
681681
}
682682

683+
func (t *Type) IsTypeParameter() bool {
684+
return t.flags&TypeFlagsTypeParameter != 0
685+
}
686+
683687
// TypeData
684688

685689
type TypeData interface {
@@ -1216,3 +1220,6 @@ var LanguageFeatureMinimumTarget = LanguageFeatureMinimumTargetMap{
12161220
ClassAndClassElementDecorators: core.ScriptTargetESNext,
12171221
RegularExpressionFlagsUnicodeSets: core.ScriptTargetESNext,
12181222
}
1223+
1224+
// Aliases for types
1225+
type StringLiteralType = Type

internal/ls/completions.go

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,8 @@ const (
256256
// true otherwise.
257257
type uniqueNamesMap = map[string]bool
258258

259-
type literalValue any // string | jsnum.Number | PseudoBigInt
259+
// string | jsnum.Number | PseudoBigInt
260+
type literalValue any
260261

261262
type globalsSearch int
262263

@@ -276,7 +277,7 @@ func (l *LanguageService) getCompletionsAtPosition(
276277
clientOptions *lsproto.CompletionClientCapabilities,
277278
) *lsproto.CompletionList {
278279
_, previousToken := getRelevantTokens(position, file)
279-
if context.TriggerCharacter != nil && !isInString(file, position, previousToken) && !isValidTrigger(file, *context.TriggerCharacter, previousToken, position) {
280+
if context.TriggerCharacter != nil && !IsInString(file, position, previousToken) && !isValidTrigger(file, *context.TriggerCharacter, previousToken, position) {
280281
return nil
281282
}
282283

@@ -294,7 +295,19 @@ func (l *LanguageService) getCompletionsAtPosition(
294295

295296
// !!! see if incomplete completion list and continue or clean
296297

297-
// !!! string literal completions
298+
stringCompletions := l.getStringLiteralCompletions(
299+
ctx,
300+
file,
301+
position,
302+
previousToken,
303+
compilerOptions,
304+
program,
305+
preferences,
306+
clientOptions,
307+
)
308+
if stringCompletions != nil {
309+
return stringCompletions
310+
}
298311

299312
if previousToken != nil && ast.IsBreakOrContinueStatement(previousToken.Parent) &&
300313
(previousToken.Kind == ast.KindBreakKeyword ||
@@ -1479,10 +1492,11 @@ func (l *LanguageService) completionInfoFromData(
14791492
clientOptions *lsproto.CompletionClientCapabilities,
14801493
) *lsproto.CompletionList {
14811494
keywordFilters := data.keywordFilters
1482-
symbols := data.symbols
14831495
isNewIdentifierLocation := data.isNewIdentifierLocation
14841496
contextToken := data.contextToken
14851497
literals := data.literals
1498+
typeChecker, done := program.GetTypeCheckerForFile(ctx, file)
1499+
defer done()
14861500

14871501
// Verify if the file is JSX language variant
14881502
if ast.GetLanguageVariant(file.ScriptKind) == core.LanguageVariantJSX {
@@ -1498,17 +1512,31 @@ func (l *LanguageService) completionInfoFromData(
14981512
if caseClause != nil &&
14991513
(contextToken.Kind == ast.KindCaseKeyword ||
15001514
ast.IsNodeDescendantOf(contextToken, caseClause.Expression())) {
1501-
// !!! switch completions
1515+
tracker := newCaseClauseTracker(typeChecker, caseClause.Parent.AsCaseBlock().Clauses.Nodes)
1516+
literals = core.Filter(literals, func(literal literalValue) bool {
1517+
return !tracker.hasValue(literal)
1518+
})
1519+
data.symbols = core.Filter(data.symbols, func(symbol *ast.Symbol) bool {
1520+
if symbol.ValueDeclaration != nil && ast.IsEnumMember(symbol.ValueDeclaration) {
1521+
value := typeChecker.GetConstantValue(symbol.ValueDeclaration)
1522+
if value != nil && tracker.hasValue(value) {
1523+
return false
1524+
}
1525+
}
1526+
return true
1527+
})
15021528
}
15031529

15041530
isChecked := isCheckedFile(file, compilerOptions)
1505-
if isChecked && !isNewIdentifierLocation && len(symbols) == 0 && keywordFilters == KeywordCompletionFiltersNone {
1531+
if isChecked && !isNewIdentifierLocation && len(data.symbols) == 0 && keywordFilters == KeywordCompletionFiltersNone {
15061532
return nil
15071533
}
15081534

1535+
optionalReplacementSpan := l.getOptionalReplacementSpan(data.location, file)
15091536
uniqueNames, sortedEntries := l.getCompletionEntriesFromSymbols(
15101537
ctx,
15111538
data,
1539+
optionalReplacementSpan,
15121540
nil, /*replacementToken*/
15131541
position,
15141542
file,
@@ -1570,6 +1598,7 @@ func (l *LanguageService) completionInfoFromData(
15701598
func (l *LanguageService) getCompletionEntriesFromSymbols(
15711599
ctx context.Context,
15721600
data *completionDataData,
1601+
optionalReplacementSpan *lsproto.Range,
15731602
replacementToken *ast.Node,
15741603
position int,
15751604
file *ast.SourceFile,
@@ -1584,7 +1613,6 @@ func (l *LanguageService) getCompletionEntriesFromSymbols(
15841613
typeChecker, done := program.GetTypeCheckerForFile(ctx, file)
15851614
defer done()
15861615
isMemberCompletion := isMemberCompletionKind(data.completionKind)
1587-
optionalReplacementSpan := getOptionalReplacementSpan(data.location, file)
15881616
// Tracks unique names.
15891617
// Value is set to false for global variables or completions from external module exports, because we can have multiple of those;
15901618
// true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports.
@@ -1708,7 +1736,7 @@ func (l *LanguageService) createCompletionItem(
17081736
preferences *UserPreferences,
17091737
clientOptions *lsproto.CompletionClientCapabilities,
17101738
isMemberCompletion bool,
1711-
optionalReplacementSpan *core.TextRange,
1739+
optionalReplacementSpan *lsproto.Range,
17121740
) *lsproto.CompletionItem {
17131741
contextToken := data.contextToken
17141742
var insertText string
@@ -3110,12 +3138,11 @@ func getJSCompletionEntries(
31103138
return sortedEntries
31113139
}
31123140

3113-
func getOptionalReplacementSpan(location *ast.Node, file *ast.SourceFile) *core.TextRange {
3141+
func (l *LanguageService) getOptionalReplacementSpan(location *ast.Node, file *ast.SourceFile) *lsproto.Range {
31143142
// StringLiteralLike locations are handled separately in stringCompletions.ts
31153143
if location != nil && location.Kind == ast.KindIdentifier {
31163144
start := astnav.GetStartOfNode(location, file, false /*includeJSDoc*/)
3117-
textRange := core.NewTextRange(start, location.End())
3118-
return &textRange
3145+
return l.createLspRangeFromBounds(start, location.End(), file)
31193146
}
31203147
return nil
31213148
}
@@ -3934,7 +3961,7 @@ func (l *LanguageService) getJsxClosingTagCompletion(
39343961
tagName := jsxClosingElement.Parent.AsJsxElement().OpeningElement.TagName()
39353962
closingTag := tagName.Text()
39363963
fullClosingTag := closingTag + core.IfElse(hasClosingAngleBracket, "", ">")
3937-
optionalReplacementSpan := core.NewTextRange(jsxClosingElement.TagName().Pos(), jsxClosingElement.TagName().End())
3964+
optionalReplacementSpan := l.createLspRangeFromNode(jsxClosingElement.TagName(), file)
39383965
defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/)
39393966

39403967
item := l.createLSPCompletionItem(
@@ -3945,7 +3972,7 @@ func (l *LanguageService) getJsxClosingTagCompletion(
39453972
ScriptElementKindClassElement,
39463973
core.Set[ScriptElementKindModifier]{}, /*kindModifiers*/
39473974
nil, /*replacementSpan*/
3948-
&optionalReplacementSpan,
3975+
optionalReplacementSpan,
39493976
nil, /*commitCharacters*/
39503977
nil, /*labelDetails*/
39513978
file,
@@ -3975,7 +4002,7 @@ func (l *LanguageService) createLSPCompletionItem(
39754002
elementKind ScriptElementKind,
39764003
kindModifiers core.Set[ScriptElementKindModifier],
39774004
replacementSpan *lsproto.Range,
3978-
optionalReplacementSpan *core.TextRange,
4005+
optionalReplacementSpan *lsproto.Range,
39794006
commitCharacters *[]string,
39804007
labelDetails *lsproto.CompletionItemLabelDetails,
39814008
file *ast.SourceFile,
@@ -4001,8 +4028,11 @@ func (l *LanguageService) createLSPCompletionItem(
40014028
} else {
40024029
// Ported from vscode ts extension.
40034030
if optionalReplacementSpan != nil && ptrIsTrue(clientOptions.CompletionItem.InsertReplaceSupport) {
4004-
insertRange := l.createLspRangeFromBounds(optionalReplacementSpan.Pos(), position, file)
4005-
replaceRange := l.createLspRangeFromBounds(optionalReplacementSpan.Pos(), optionalReplacementSpan.End(), file)
4031+
insertRange := &lsproto.Range{
4032+
Start: optionalReplacementSpan.Start,
4033+
End: l.createLspPosition(position, file),
4034+
}
4035+
replaceRange := optionalReplacementSpan
40064036
textEdit = &lsproto.TextEditOrInsertReplaceEdit{
40074037
InsertReplaceEdit: &lsproto.InsertReplaceEdit{
40084038
NewText: core.IfElse(insertText == "", name, insertText),
@@ -4085,11 +4115,10 @@ func (l *LanguageService) createLSPCompletionItem(
40854115
// !!! adjust label like vscode does
40864116
}
40874117

4118+
// Client assumes plain text by default.
40884119
var insertTextFormat *lsproto.InsertTextFormat
40894120
if isSnippet {
40904121
insertTextFormat = ptrTo(lsproto.InsertTextFormatSnippet)
4091-
} else {
4092-
insertTextFormat = ptrTo(lsproto.InsertTextFormatPlainText)
40934122
}
40944123

40954124
return &lsproto.CompletionItem{

0 commit comments

Comments
 (0)