@@ -121,7 +121,7 @@ const (
121
121
SortTextJavascriptIdentifiers sortText = "18"
122
122
)
123
123
124
- func deprecateSortText (original sortText ) sortText {
124
+ func DeprecateSortText (original sortText ) sortText {
125
125
return "z" + original
126
126
}
127
127
@@ -910,6 +910,7 @@ func (l *LanguageService) getCompletionEntriesFromSymbols(
910
910
closestSymbolDeclaration := getClosestSymbolDeclaration (data .contextToken , data .location )
911
911
useSemicolons := probablyUsesSemicolons (file )
912
912
typeChecker := program .GetTypeChecker ()
913
+ isMemberCompletion := isMemberCompletionKind (data .completionKind )
913
914
// Tracks unique names.
914
915
// Value is set to false for global variables or completions from external module exports, because we can have multiple of those;
915
916
// 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(
944
945
945
946
var sortText sortText
946
947
if isDeprecated (symbol , typeChecker ) {
947
- sortText = deprecateSortText (originalSortText )
948
+ sortText = DeprecateSortText (originalSortText )
948
949
} else {
949
950
sortText = originalSortText
950
951
}
@@ -963,12 +964,13 @@ func (l *LanguageService) getCompletionEntriesFromSymbols(
963
964
compilerOptions ,
964
965
preferences ,
965
966
clientOptions ,
967
+ isMemberCompletion ,
966
968
)
967
969
if entry == nil {
968
970
continue
969
971
}
970
972
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.
972
974
shouldShadowLaterSymbols := (origin == nil || originIsTypeOnlyAlias (origin )) &&
973
975
! (symbol .Parent == nil &&
974
976
! core .Some (symbol .Declarations , func (d * ast.Node ) bool { return ast .GetSourceFileOfNode (d ) == file }))
@@ -1028,6 +1030,7 @@ func (l *LanguageService) createCompletionItem(
1028
1030
compilerOptions * core.CompilerOptions ,
1029
1031
preferences * UserPreferences ,
1030
1032
clientOptions * lsproto.CompletionClientCapabilities ,
1033
+ isMemberCompletion bool ,
1031
1034
) * lsproto.CompletionItem {
1032
1035
contextToken := data .contextToken
1033
1036
var insertText string
@@ -1250,6 +1253,8 @@ func (l *LanguageService) createCompletionItem(
1250
1253
}
1251
1254
}
1252
1255
1256
+ // Commit characters
1257
+
1253
1258
elementKind := getSymbolKind (typeChecker , symbol , data .location )
1254
1259
kind := getCompletionsSymbolKind (elementKind )
1255
1260
var commitCharacters * []string
@@ -1262,10 +1267,77 @@ func (l *LanguageService) createCompletionItem(
1262
1267
// Otherwise use the completion list default.
1263
1268
}
1264
1269
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
+
1265
1337
kindModifiers := getSymbolModifiers (typeChecker , symbol )
1266
1338
var tags * []lsproto.CompletionItemTag
1267
1339
var detail * string
1268
- // Copied from vscode ts extension.
1340
+ // Copied from vscode ts extension: `MyCompletionItem.constructor` .
1269
1341
if kindModifiers .Has (ScriptElementKindModifierOptional ) {
1270
1342
if insertText == "" {
1271
1343
insertText = name
@@ -1302,17 +1374,6 @@ func (l *LanguageService) createCompletionItem(
1302
1374
insertTextFormat = ptrTo (lsproto .InsertTextFormatPlainText )
1303
1375
}
1304
1376
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
-
1316
1377
return & lsproto.CompletionItem {
1317
1378
Label : name ,
1318
1379
LabelDetails : labelDetails ,
@@ -1340,6 +1401,118 @@ func isRecommendedCompletionMatch(localSymbol *ast.Symbol, recommendedCompletion
1340
1401
localSymbol .Flags & ast .SymbolFlagsExportValue != 0 && typeChecker .GetExportSymbolOfSymbol (localSymbol ) == recommendedCompletion
1341
1402
}
1342
1403
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
+
1343
1516
func strPtrTo (v string ) * string {
1344
1517
if v == "" {
1345
1518
return nil
@@ -2319,3 +2492,19 @@ func getJSCompletionEntries(
2319
2492
}
2320
2493
return sortedEntries
2321
2494
}
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