Skip to content

Commit 83fd62c

Browse files
authored
Port more of member completions + tests (#833)
1 parent 54aed8f commit 83fd62c

File tree

6 files changed

+1177
-72
lines changed

6 files changed

+1177
-72
lines changed

internal/ast/utilities.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2792,3 +2792,12 @@ func IsClassMemberModifier(token Kind) bool {
27922792
func IsParameterPropertyModifier(kind Kind) bool {
27932793
return ModifierToFlag(kind)&ModifierFlagsParameterPropertyModifier != 0
27942794
}
2795+
2796+
func ForEachChildAndJSDoc(node *Node, sourceFile *SourceFile, v Visitor) bool {
2797+
if node.Flags&NodeFlagsHasJSDoc != 0 {
2798+
if visitNodes(v, node.JSDoc(sourceFile)) {
2799+
return true
2800+
}
2801+
}
2802+
return node.ForEachChild(v)
2803+
}

internal/ls/completions.go

Lines changed: 204 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ const (
121121
SortTextJavascriptIdentifiers sortText = "18"
122122
)
123123

124-
func deprecateSortText(original sortText) sortText {
124+
func DeprecateSortText(original sortText) sortText {
125125
return "z" + original
126126
}
127127

@@ -910,6 +910,7 @@ func (l *LanguageService) getCompletionEntriesFromSymbols(
910910
closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location)
911911
useSemicolons := probablyUsesSemicolons(file)
912912
typeChecker := program.GetTypeChecker()
913+
isMemberCompletion := isMemberCompletionKind(data.completionKind)
913914
// Tracks unique names.
914915
// Value is set to false for global variables or completions from external module exports, because we can have multiple of those;
915916
// true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports.
@@ -944,7 +945,7 @@ func (l *LanguageService) getCompletionEntriesFromSymbols(
944945

945946
var sortText sortText
946947
if isDeprecated(symbol, typeChecker) {
947-
sortText = deprecateSortText(originalSortText)
948+
sortText = DeprecateSortText(originalSortText)
948949
} else {
949950
sortText = originalSortText
950951
}
@@ -963,12 +964,13 @@ func (l *LanguageService) getCompletionEntriesFromSymbols(
963964
compilerOptions,
964965
preferences,
965966
clientOptions,
967+
isMemberCompletion,
966968
)
967969
if entry == nil {
968970
continue
969971
}
970972

971-
/** True for locals; false for globals, module exports from other files, `this.` completions. */
973+
// True for locals; false for globals, module exports from other files, `this.` completions.
972974
shouldShadowLaterSymbols := (origin == nil || originIsTypeOnlyAlias(origin)) &&
973975
!(symbol.Parent == nil &&
974976
!core.Some(symbol.Declarations, func(d *ast.Node) bool { return ast.GetSourceFileOfNode(d) == file }))
@@ -1028,6 +1030,7 @@ func (l *LanguageService) createCompletionItem(
10281030
compilerOptions *core.CompilerOptions,
10291031
preferences *UserPreferences,
10301032
clientOptions *lsproto.CompletionClientCapabilities,
1033+
isMemberCompletion bool,
10311034
) *lsproto.CompletionItem {
10321035
contextToken := data.contextToken
10331036
var insertText string
@@ -1250,6 +1253,8 @@ func (l *LanguageService) createCompletionItem(
12501253
}
12511254
}
12521255

1256+
// Commit characters
1257+
12531258
elementKind := getSymbolKind(typeChecker, symbol, data.location)
12541259
kind := getCompletionsSymbolKind(elementKind)
12551260
var commitCharacters *[]string
@@ -1262,10 +1267,77 @@ func (l *LanguageService) createCompletionItem(
12621267
// Otherwise use the completion list default.
12631268
}
12641269

1270+
// Text edit
1271+
1272+
var textEdit *lsproto.TextEditOrInsertReplaceEdit
1273+
if replacementSpan != nil {
1274+
textEdit = &lsproto.TextEditOrInsertReplaceEdit{
1275+
TextEdit: &lsproto.TextEdit{
1276+
NewText: core.IfElse(insertText == "", name, insertText),
1277+
Range: *replacementSpan,
1278+
},
1279+
}
1280+
} else {
1281+
// Ported from vscode ts extension.
1282+
optionalReplacementSpan := getOptionalReplacementSpan(data.location, file)
1283+
if optionalReplacementSpan != nil && ptrIsTrue(clientOptions.CompletionItem.InsertReplaceSupport) {
1284+
insertRange := l.createLspRangeFromBounds(optionalReplacementSpan.Pos(), position, file)
1285+
replaceRange := l.createLspRangeFromBounds(optionalReplacementSpan.Pos(), optionalReplacementSpan.End(), file)
1286+
textEdit = &lsproto.TextEditOrInsertReplaceEdit{
1287+
InsertReplaceEdit: &lsproto.InsertReplaceEdit{
1288+
NewText: core.IfElse(insertText == "", name, insertText),
1289+
Insert: *insertRange,
1290+
Replace: *replaceRange,
1291+
},
1292+
}
1293+
}
1294+
}
1295+
1296+
// Filter text
1297+
1298+
// Ported from vscode ts extension.
1299+
wordRange, wordStart := getWordRange(file, position)
1300+
if filterText == "" {
1301+
filterText = getFilterText(file, position, insertText, name, isMemberCompletion, isSnippet, wordStart)
1302+
}
1303+
if isMemberCompletion && !isSnippet {
1304+
accessorRange, accessorText := getDotAccessorContext(file, position)
1305+
if accessorText != "" {
1306+
filterText = accessorText + core.IfElse(insertText != "", insertText, name)
1307+
if textEdit == nil {
1308+
insertText = filterText
1309+
if wordRange != nil && ptrIsTrue(clientOptions.CompletionItem.InsertReplaceSupport) {
1310+
textEdit = &lsproto.TextEditOrInsertReplaceEdit{
1311+
InsertReplaceEdit: &lsproto.InsertReplaceEdit{
1312+
NewText: insertText,
1313+
Insert: *l.createLspRangeFromBounds(
1314+
accessorRange.Pos(),
1315+
accessorRange.End(),
1316+
file),
1317+
Replace: *l.createLspRangeFromBounds(
1318+
min(accessorRange.Pos(), wordRange.Pos()),
1319+
accessorRange.End(),
1320+
file),
1321+
},
1322+
}
1323+
} else {
1324+
textEdit = &lsproto.TextEditOrInsertReplaceEdit{
1325+
TextEdit: &lsproto.TextEdit{
1326+
NewText: insertText,
1327+
Range: *l.createLspRangeFromBounds(accessorRange.Pos(), accessorRange.End(), file),
1328+
},
1329+
}
1330+
}
1331+
}
1332+
}
1333+
}
1334+
1335+
// Adjustements based on kind modifiers.
1336+
12651337
kindModifiers := getSymbolModifiers(typeChecker, symbol)
12661338
var tags *[]lsproto.CompletionItemTag
12671339
var detail *string
1268-
// Copied from vscode ts extension.
1340+
// Copied from vscode ts extension: `MyCompletionItem.constructor`.
12691341
if kindModifiers.Has(ScriptElementKindModifierOptional) {
12701342
if insertText == "" {
12711343
insertText = name
@@ -1302,17 +1374,6 @@ func (l *LanguageService) createCompletionItem(
13021374
insertTextFormat = ptrTo(lsproto.InsertTextFormatPlainText)
13031375
}
13041376

1305-
var textEdit *lsproto.TextEditOrInsertReplaceEdit
1306-
if replacementSpan != nil {
1307-
textEdit = &lsproto.TextEditOrInsertReplaceEdit{
1308-
TextEdit: &lsproto.TextEdit{
1309-
NewText: core.IfElse(insertText == "", name, insertText),
1310-
Range: *replacementSpan,
1311-
},
1312-
}
1313-
}
1314-
// !!! adjust text edit like vscode does when Strada's `isMemberCompletion` is true
1315-
13161377
return &lsproto.CompletionItem{
13171378
Label: name,
13181379
LabelDetails: labelDetails,
@@ -1340,6 +1401,118 @@ func isRecommendedCompletionMatch(localSymbol *ast.Symbol, recommendedCompletion
13401401
localSymbol.Flags&ast.SymbolFlagsExportValue != 0 && typeChecker.GetExportSymbolOfSymbol(localSymbol) == recommendedCompletion
13411402
}
13421403

1404+
// Ported from vscode's `USUAL_WORD_SEPARATORS`.
1405+
var wordSeparators = core.NewSetFromItems(
1406+
'`', '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '=', '+', '[', '{', ']', '}', '\\', '|',
1407+
';', ':', '\'', '"', ',', '.', '<', '>', '/', '?',
1408+
)
1409+
1410+
// Finds the range of the word that ends at the given position.
1411+
// e.g. for "abc def.ghi|jkl", the word range is "ghi" and the word start is 'g'.
1412+
func getWordRange(sourceFile *ast.SourceFile, position int) (wordRange *core.TextRange, wordStart rune) {
1413+
// !!! Port other case of vscode's `DEFAULT_WORD_REGEXP` that covers words that start like numbers, e.g. -123.456abcd.
1414+
text := sourceFile.Text()[:position]
1415+
totalSize := 0
1416+
var firstRune rune
1417+
for r, size := utf8.DecodeLastRuneInString(text); size != 0; r, size = utf8.DecodeLastRuneInString(text[:len(text)-size]) {
1418+
if wordSeparators.Has(r) || unicode.IsSpace(r) {
1419+
break
1420+
}
1421+
totalSize += size
1422+
firstRune = r
1423+
}
1424+
// If word starts with `@`, disregard this first character.
1425+
if firstRune == '@' {
1426+
totalSize -= 1
1427+
firstRune, _ = utf8.DecodeRuneInString(text[len(text)-totalSize:])
1428+
}
1429+
if totalSize == 0 {
1430+
return nil, firstRune
1431+
}
1432+
textRange := core.NewTextRange(position-totalSize, position)
1433+
return &textRange, firstRune
1434+
}
1435+
1436+
// Ported from vscode ts extension: `getFilterText`.
1437+
func getFilterText(
1438+
file *ast.SourceFile,
1439+
position int,
1440+
insertText string,
1441+
label string,
1442+
isMemberCompletion bool,
1443+
isSnippet bool,
1444+
wordStart rune,
1445+
) string {
1446+
// Private field completion.
1447+
if strings.HasPrefix(label, "#") {
1448+
// !!! document theses cases
1449+
if insertText != "" {
1450+
if strings.HasPrefix(insertText, "this.#") {
1451+
if wordStart == '#' {
1452+
return insertText
1453+
} else {
1454+
return strings.TrimPrefix(insertText, "this.#")
1455+
}
1456+
}
1457+
} else {
1458+
if wordStart == '#' {
1459+
return ""
1460+
} else {
1461+
return strings.TrimPrefix(label, "#")
1462+
}
1463+
}
1464+
}
1465+
1466+
// For `this.` completions, generally don't set the filter text since we don't want them to be overly prioritized. microsoft/vscode#74164
1467+
if strings.HasPrefix(insertText, "this.") {
1468+
return ""
1469+
}
1470+
1471+
// Handle the case:
1472+
// ```
1473+
// const xyz = { 'ab c': 1 };
1474+
// xyz.ab|
1475+
// ```
1476+
// In which case we want to insert a bracket accessor but should use `.abc` as the filter text instead of
1477+
// the bracketed insert text.
1478+
if strings.HasPrefix(insertText, "[") {
1479+
if strings.HasPrefix(insertText, `['`) && strings.HasSuffix(insertText, `']`) {
1480+
return "." + strings.TrimPrefix(strings.TrimSuffix(insertText, `']`), `['`)
1481+
}
1482+
if strings.HasPrefix(insertText, `["`) && strings.HasSuffix(insertText, `"]`) {
1483+
return "." + strings.TrimPrefix(strings.TrimSuffix(insertText, `"]`), `["`)
1484+
}
1485+
return insertText
1486+
}
1487+
1488+
// In all other cases, fall back to using the insertText.
1489+
return insertText
1490+
}
1491+
1492+
// Ported from vscode's `provideCompletionItems`.
1493+
func getDotAccessorContext(file *ast.SourceFile, position int) (acessorRange *core.TextRange, accessorText string) {
1494+
text := file.Text()[:position]
1495+
totalSize := 0
1496+
for r, size := utf8.DecodeLastRuneInString(text); size != 0; r, size = utf8.DecodeLastRuneInString(text[:len(text)-size]) {
1497+
if !unicode.IsSpace(r) {
1498+
break
1499+
}
1500+
totalSize += size
1501+
text = text[:len(text)-size]
1502+
}
1503+
if strings.HasSuffix(text, "?.") {
1504+
totalSize += 2
1505+
newRange := core.NewTextRange(position-totalSize, position)
1506+
return &newRange, file.Text()[position-totalSize : position]
1507+
}
1508+
if strings.HasSuffix(text, ".") {
1509+
totalSize += 1
1510+
newRange := core.NewTextRange(position-totalSize, position)
1511+
return &newRange, file.Text()[position-totalSize : position]
1512+
}
1513+
return nil, ""
1514+
}
1515+
13431516
func strPtrTo(v string) *string {
13441517
if v == "" {
13451518
return nil
@@ -2319,3 +2492,19 @@ func getJSCompletionEntries(
23192492
}
23202493
return sortedEntries
23212494
}
2495+
2496+
func getOptionalReplacementSpan(location *ast.Node, file *ast.SourceFile) *core.TextRange {
2497+
// StringLiteralLike locations are handled separately in stringCompletions.ts
2498+
if location != nil && location.Kind == ast.KindIdentifier {
2499+
start := astnav.GetStartOfNode(location, file, false /*includeJSDoc*/)
2500+
textRange := core.NewTextRange(start, location.End())
2501+
return &textRange
2502+
}
2503+
return nil
2504+
}
2505+
2506+
func isMemberCompletionKind(kind CompletionKind) bool {
2507+
return kind == CompletionKindObjectPropertyDeclaration ||
2508+
kind == CompletionKindMemberLike ||
2509+
kind == CompletionKindPropertyAccess
2510+
}

0 commit comments

Comments
 (0)