package ls import ( "context" "errors" "fmt" "maps" "slices" "strings" "sync" "unicode" "unicode/utf8" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/binder" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/format" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/jsnum" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsutil" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/nodebuilder" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" "github.com/go-json-experiment/json" ) func (l *LanguageService) ProvideCompletion( ctx context.Context, documentURI lsproto.DocumentUri, LSPPosition lsproto.Position, context *lsproto.CompletionContext, clientOptions *lsproto.CompletionClientCapabilities, preferences *UserPreferences, ) (lsproto.CompletionResponse, error) { _, file := l.getProgramAndFile(documentURI) var triggerCharacter *string if context != nil { triggerCharacter = context.TriggerCharacter } position := int(l.converters.LineAndCharacterToPosition(file, LSPPosition)) completionList := l.getCompletionsAtPosition( ctx, file, position, triggerCharacter, preferences, clientOptions, ) completionList = ensureItemData(file.FileName(), position, completionList) return lsproto.CompletionItemsOrListOrNull{List: completionList}, nil } func ensureItemData(fileName string, pos int, list *lsproto.CompletionList) *lsproto.CompletionList { if list == nil { return nil } for _, item := range list.Items { if item.Data == nil { var data any = &itemData{ FileName: fileName, Position: pos, Name: item.Label, } item.Data = &data } } return list } // *completionDataData | *completionDataKeyword | *completionDataJSDocTagName | *completionDataJSDocTag | *completionDataJSDocParameterName type completionData = any type completionDataData struct { symbols []*ast.Symbol completionKind CompletionKind isInSnippetScope bool // Note that the presence of this alone doesn't mean that we need a conversion. Only do that if the completion is not an ordinary identifier. propertyAccessToConvert *ast.PropertyAccessExpressionNode isNewIdentifierLocation bool location *ast.Node keywordFilters KeywordCompletionFilters literals []literalValue symbolToOriginInfoMap map[ast.SymbolId]*symbolOriginInfo symbolToSortTextMap map[ast.SymbolId]sortText recommendedCompletion *ast.Symbol previousToken *ast.Node contextToken *ast.Node jsxInitializer jsxInitializer insideJSDocTagTypeExpression bool isTypeOnlyLocation bool // In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. isJsxIdentifierExpected bool isRightOfOpenTag bool isRightOfDotOrQuestionDot bool importStatementCompletion *importStatementCompletionInfo // !!! hasUnresolvedAutoImports bool // !!! // flags CompletionInfoFlags // !!! defaultCommitCharacters []string } type completionDataKeyword struct { keywordCompletions []*lsproto.CompletionItem isNewIdentifierLocation bool } type completionDataJSDocTagName struct{} type completionDataJSDocTag struct{} type completionDataJSDocParameterName struct { tag *ast.JSDocParameterTag } type importStatementCompletionInfo struct { isKeywordOnlyCompletion bool keywordCompletion ast.Kind // TokenKind isNewIdentifierLocation bool isTopLevelTypeOnly bool couldBeTypeOnlyImportSpecifier bool replacementSpan *lsproto.Range } // If we're after the `=` sign but no identifier has been typed yet, // value will be `true` but initializer will be `nil`. type jsxInitializer struct { isInitializer bool initializer *ast.IdentifierNode } type KeywordCompletionFilters int const ( KeywordCompletionFiltersNone KeywordCompletionFilters = iota // No keywords KeywordCompletionFiltersAll // Every possible kewyord KeywordCompletionFiltersClassElementKeywords // Keywords inside class body KeywordCompletionFiltersInterfaceElementKeywords // Keywords inside interface body KeywordCompletionFiltersConstructorParameterKeywords // Keywords at constructor parameter KeywordCompletionFiltersFunctionLikeBodyKeywords // Keywords at function like body KeywordCompletionFiltersTypeAssertionKeywords KeywordCompletionFiltersTypeKeywords KeywordCompletionFiltersTypeKeyword // Literally just `type` KeywordCompletionFiltersLast = KeywordCompletionFiltersTypeKeyword ) func keywordFiltersFromSyntaxKind(keywordCompletion ast.Kind) KeywordCompletionFilters { switch keywordCompletion { case ast.KindTypeKeyword: return KeywordCompletionFiltersTypeKeyword default: panic("Unknown mapping from ast.Kind `" + keywordCompletion.String() + "` to KeywordCompletionFilters") } } type CompletionKind int const ( CompletionKindNone CompletionKind = iota CompletionKindObjectPropertyDeclaration CompletionKindGlobal CompletionKindPropertyAccess CompletionKindMemberLike CompletionKindString ) var TriggerCharacters = []string{".", `"`, "'", "`", "/", "@", "<", "#", " "} // All commit characters, valid when `isNewIdentifierLocation` is false. var allCommitCharacters = []string{".", ",", ";"} // Commit characters valid at expression positions where we could be inside a parameter list. var noCommaCommitCharacters = []string{".", ";"} var emptyCommitCharacters = []string{} type sortText string const ( SortTextLocalDeclarationPriority sortText = "10" SortTextLocationPriority sortText = "11" SortTextOptionalMember sortText = "12" SortTextMemberDeclaredBySpreadAssignment sortText = "13" SortTextSuggestedClassMembers sortText = "14" SortTextGlobalsOrKeywords sortText = "15" SortTextAutoImportSuggestions sortText = "16" SortTextClassMemberSnippets sortText = "17" SortTextJavascriptIdentifiers sortText = "18" ) func DeprecateSortText(original sortText) sortText { return "z" + original } func sortBelow(original sortText) sortText { return original + "1" } type symbolOriginInfoKind int const ( symbolOriginInfoKindThisType symbolOriginInfoKind = 1 << iota symbolOriginInfoKindSymbolMember symbolOriginInfoKindExport symbolOriginInfoKindPromise symbolOriginInfoKindNullable symbolOriginInfoKindTypeOnlyAlias symbolOriginInfoKindObjectLiteralMethod symbolOriginInfoKindIgnore symbolOriginInfoKindComputedPropertyName symbolOriginInfoKindSymbolMemberNoExport symbolOriginInfoKind = symbolOriginInfoKindSymbolMember symbolOriginInfoKindSymbolMemberExport = symbolOriginInfoKindSymbolMember | symbolOriginInfoKindExport ) type symbolOriginInfo struct { kind symbolOriginInfoKind isDefaultExport bool isFromPackageJson bool fileName string data any } func (origin *symbolOriginInfo) symbolName() string { switch origin.data.(type) { case *symbolOriginInfoExport: return origin.data.(*symbolOriginInfoExport).symbolName case *symbolOriginInfoComputedPropertyName: return origin.data.(*symbolOriginInfoComputedPropertyName).symbolName default: panic(fmt.Sprintf("symbolOriginInfo: unknown data type for symbolName(): %T", origin.data)) } } func (origin *symbolOriginInfo) moduleSymbol() *ast.Symbol { switch origin.data.(type) { case *symbolOriginInfoExport: return origin.data.(*symbolOriginInfoExport).moduleSymbol default: panic(fmt.Sprintf("symbolOriginInfo: unknown data type for moduleSymbol(): %T", origin.data)) } } func (origin *symbolOriginInfo) toCompletionEntryData() *completionEntryData { debug.Assert(origin.kind&symbolOriginInfoKindExport != 0, fmt.Sprintf("completionEntryData is not generated for symbolOriginInfo of type %T", origin.data)) var ambientModuleName *string if origin.fileName == "" { ambientModuleName = strPtrTo(stringutil.StripQuotes(origin.moduleSymbol().Name)) } var isPackageJsonImport core.Tristate if origin.isFromPackageJson { isPackageJsonImport = core.TSTrue } data := origin.data.(*symbolOriginInfoExport) return &completionEntryData{ ExportName: data.exportName, ExportMapKey: data.exportMapKey, ModuleSpecifier: data.moduleSpecifier, AmbientModuleName: ambientModuleName, FileName: strPtrTo(origin.fileName), IsPackageJsonImport: isPackageJsonImport, } } type symbolOriginInfoExport struct { symbolName string moduleSymbol *ast.Symbol exportName string exportMapKey ExportInfoMapKey moduleSpecifier string } func (s *symbolOriginInfo) asExport() *symbolOriginInfoExport { return s.data.(*symbolOriginInfoExport) } type symbolOriginInfoObjectLiteralMethod struct { insertText string labelDetails *lsproto.CompletionItemLabelDetails isSnippet bool } func (s *symbolOriginInfo) asObjectLiteralMethod() *symbolOriginInfoObjectLiteralMethod { return s.data.(*symbolOriginInfoObjectLiteralMethod) } type symbolOriginInfoTypeOnlyAlias struct { declaration *ast.TypeOnlyImportDeclaration } type symbolOriginInfoComputedPropertyName struct { symbolName string } // Special values for `CompletionInfo['source']` used to disambiguate // completion items with the same `name`. (Each completion item must // have a unique name/source combination, because those two fields // comprise `CompletionEntryIdentifier` in `getCompletionEntryDetails`. // // When the completion item is an auto-import suggestion, the source // is the module specifier of the suggestion. To avoid collisions, // the values here should not be a module specifier we would ever // generate for an auto-import. type completionSource string const ( // Completions that require `this.` insertion text. completionSourceThisProperty completionSource = "ThisProperty/" // Auto-import that comes attached to a class member snippet. completionSourceClassMemberSnippet completionSource = "ClassMemberSnippet/" // A type-only import that needs to be promoted in order to be used at the completion location. completionSourceTypeOnlyAlias completionSource = "TypeOnlyAlias/" // Auto-import that comes attached to an object literal method snippet. completionSourceObjectLiteralMethodSnippet completionSource = "ObjectLiteralMethodSnippet/" // Case completions for switch statements. completionSourceSwitchCases completionSource = "SwitchCases/" // Completions for an object literal expression. completionSourceObjectLiteralMemberWithComma completionSource = "ObjectLiteralMemberWithComma/" ) // Value is set to false for global variables or completions from external module exports, // true otherwise. type uniqueNamesMap = map[string]bool // string | jsnum.Number | PseudoBigInt type literalValue any type globalsSearch int const ( globalsSearchContinue globalsSearch = iota globalsSearchSuccess globalsSearchFail ) func (l *LanguageService) getCompletionsAtPosition( ctx context.Context, file *ast.SourceFile, position int, triggerCharacter *string, preferences *UserPreferences, clientOptions *lsproto.CompletionClientCapabilities, ) *lsproto.CompletionList { _, previousToken := getRelevantTokens(position, file) if triggerCharacter != nil && !IsInString(file, position, previousToken) && !isValidTrigger(file, *triggerCharacter, previousToken, position) { return nil } if triggerCharacter != nil && *triggerCharacter == " " { // `isValidTrigger` ensures we are at `import |` if preferences.IncludeCompletionsForImportStatements.IsTrue() { return &lsproto.CompletionList{ IsIncomplete: true, } } return nil } compilerOptions := l.GetProgram().Options() // !!! see if incomplete completion list and continue or clean stringCompletions := l.getStringLiteralCompletions( ctx, file, position, previousToken, compilerOptions, preferences, clientOptions, ) if stringCompletions != nil { return stringCompletions } if previousToken != nil && (previousToken.Kind == ast.KindBreakKeyword || previousToken.Kind == ast.KindContinueKeyword || previousToken.Kind == ast.KindIdentifier) && ast.IsBreakOrContinueStatement(previousToken.Parent) { return l.getLabelCompletionsAtPosition( previousToken.Parent, clientOptions, file, position, l.getOptionalReplacementSpan(previousToken, file), ) } checker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) defer done() data := l.getCompletionData(ctx, checker, file, position, preferences) if data == nil { return nil } switch data := data.(type) { case *completionDataData: optionalReplacementSpan := l.getOptionalReplacementSpan(data.location, file) response := l.completionInfoFromData( ctx, checker, file, compilerOptions, data, preferences, position, clientOptions, optionalReplacementSpan, ) // !!! check if response is incomplete return response case *completionDataKeyword: optionalReplacementSpan := l.getOptionalReplacementSpan(previousToken, file) return l.specificKeywordCompletionInfo( clientOptions, position, file, data.keywordCompletions, data.isNewIdentifierLocation, optionalReplacementSpan, ) case *completionDataJSDocTagName: // If the current position is a jsDoc tag name, only tag names should be provided for completion items := getJSDocTagNameCompletions() items = append(items, getJSDocParameterCompletions( clientOptions, file, position, checker, compilerOptions, preferences, /*tagNameOnly*/ true, )...) return l.jsDocCompletionInfo(clientOptions, position, file, items) case *completionDataJSDocTag: // If the current position is a jsDoc tag, only tags should be provided for completion items := getJSDocTagCompletions() items = append(items, getJSDocParameterCompletions( clientOptions, file, position, checker, compilerOptions, preferences, /*tagNameOnly*/ false, )...) return l.jsDocCompletionInfo(clientOptions, position, file, items) case *completionDataJSDocParameterName: return l.jsDocCompletionInfo(clientOptions, position, file, getJSDocParameterNameCompletions(data.tag)) default: panic("getCompletionData() returned unexpected type: " + fmt.Sprintf("%T", data)) } } func (l *LanguageService) getCompletionData( ctx context.Context, typeChecker *checker.Checker, file *ast.SourceFile, position int, preferences *UserPreferences, ) completionData { inCheckedFile := isCheckedFile(file, l.GetProgram().Options()) currentToken := astnav.GetTokenAtPosition(file, position) insideComment := isInComment(file, position, currentToken) insideJSDocTagTypeExpression := false insideJsDocImportTag := false isInSnippetScope := false if insideComment != nil { if hasDocComment(file, position) { if file.Text()[position] == '@' { // The current position is next to the '@' sign, when no tag name being provided yet. // Provide a full list of tag names return &completionDataJSDocTagName{} } else { // When completion is requested without "@", we will have check to make sure that // there are no comments prefix the request position. We will only allow "*" and space. // e.g // /** |c| /* // // /** // |c| // */ // // /** // * |c| // */ // // /** // * |c| // */ lineStart := format.GetLineStartPositionForPosition(position, file) noCommentPrefix := true for _, r := range file.Text()[lineStart:position] { if !(stringutil.IsWhiteSpaceSingleLine(r) || r == '*' || r == '/' || r == '(' || r == ')' || r == '|') { noCommentPrefix = false break } } if noCommentPrefix { return &completionDataJSDocTag{} } } } // Completion should work inside certain JSDoc tags. For example: // /** @type {number | string} */ // Completion should work in the brackets if tag := getJSDocTagAtPosition(currentToken, position); tag != nil { if tag.TagName().Pos() <= position && position <= tag.TagName().End() { return &completionDataJSDocTagName{} } if ast.IsJSDocImportTag(tag) { insideJsDocImportTag = true } else { if typeExpression := tryGetTypeExpressionFromTag(tag); typeExpression != nil { currentToken = astnav.GetTokenAtPosition(file, position) if currentToken == nil || (!ast.IsDeclarationName(currentToken) && (currentToken.Parent.Kind != ast.KindJSDocPropertyTag || currentToken.Parent.Name() != currentToken)) { // Use as type location if inside tag's type expression insideJSDocTagTypeExpression = isCurrentlyEditingNode(typeExpression, file, position) } } if !insideJSDocTagTypeExpression && ast.IsJSDocParameterTag(tag) && (ast.NodeIsMissing(tag.Name()) || tag.Name().Pos() <= position && position <= tag.Name().End()) { return &completionDataJSDocParameterName{ tag: tag.AsJSDocParameterOrPropertyTag(), } } } } if !insideJSDocTagTypeExpression && !insideJsDocImportTag { // Proceed if the current position is in JSDoc tag expression; otherwise it is a normal // comment or the plain text part of a JSDoc comment, so no completion should be available return nil } } // The decision to provide completion depends on the contextToken, which is determined through the previousToken. // Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file isJSOnlyLocation := !insideJSDocTagTypeExpression && !insideJsDocImportTag && ast.IsSourceFileJS(file) contextToken, previousToken := getRelevantTokens(position, file) // Find the node where completion is requested on. // Also determine whether we are trying to complete with members of that node // or attributes of a JSX tag. node := currentToken var propertyAccessToConvert *ast.PropertyAccessExpressionNode isRightOfDot := false isRightOfQuestionDot := false isRightOfOpenTag := false isStartingCloseTag := false var jsxInitializer jsxInitializer isJsxIdentifierExpected := false var importStatementCompletion *importStatementCompletionInfo location := astnav.GetTouchingPropertyName(file, position) keywordFilters := KeywordCompletionFiltersNone isNewIdentifierLocation := false // !!! flags := CompletionInfoFlagsNone var defaultCommitCharacters []string if contextToken != nil { importStatementCompletionInfo := l.getImportStatementCompletionInfo(contextToken, file) if importStatementCompletionInfo.keywordCompletion != ast.KindUnknown { if importStatementCompletionInfo.isKeywordOnlyCompletion { return &completionDataKeyword{ keywordCompletions: []*lsproto.CompletionItem{{ Label: scanner.TokenToString(importStatementCompletionInfo.keywordCompletion), Kind: ptrTo(lsproto.CompletionItemKindKeyword), SortText: ptrTo(string(SortTextGlobalsOrKeywords)), }}, isNewIdentifierLocation: importStatementCompletionInfo.isNewIdentifierLocation, } } keywordFilters = keywordFiltersFromSyntaxKind(importStatementCompletionInfo.keywordCompletion) } if importStatementCompletionInfo.replacementSpan != nil && preferences.IncludeCompletionsForImportStatements.IsTrue() { // !!! flags |= CompletionInfoFlags.IsImportStatementCompletion; importStatementCompletion = &importStatementCompletionInfo isNewIdentifierLocation = importStatementCompletionInfo.isNewIdentifierLocation } // Bail out if this is a known invalid completion location. if isCompletionListBlocker(contextToken, previousToken, location, file, position, typeChecker) { if keywordFilters != KeywordCompletionFiltersNone { isNewIdentifierLocation, _ := computeCommitCharactersAndIsNewIdentifier(contextToken, file, position) return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation) } return nil } parent := contextToken.Parent if contextToken.Kind == ast.KindDotToken || contextToken.Kind == ast.KindQuestionDotToken { isRightOfDot = contextToken.Kind == ast.KindDotToken isRightOfQuestionDot = contextToken.Kind == ast.KindQuestionDotToken switch parent.Kind { case ast.KindPropertyAccessExpression: propertyAccessToConvert = parent node = propertyAccessToConvert.Expression() leftMostAccessExpression := ast.GetLeftmostAccessExpression(parent) if ast.NodeIsMissing(leftMostAccessExpression) || ((ast.IsCallExpression(node) || ast.IsFunctionLike(node)) && node.End() == contextToken.Pos() && lsutil.GetLastChild(node, file).Kind != ast.KindCloseParenToken) { // This is likely dot from incorrectly parsed expression and user is starting to write spread // eg: Math.min(./**/) // const x = function (./**/) {} // ({./**/}) return nil } case ast.KindQualifiedName: node = parent.AsQualifiedName().Left case ast.KindModuleDeclaration: node = parent.Name() case ast.KindImportType: node = parent case ast.KindMetaProperty: node = lsutil.GetFirstToken(parent, file) if node.Kind != ast.KindImportKeyword && node.Kind != ast.KindNewKeyword { panic("Unexpected token kind: " + node.Kind.String()) } default: // There is nothing that precedes the dot, so this likely just a stray character // or leading into a '...' token. Just bail out instead. return nil } } else { // !!! else if (!importStatementCompletion) // // If the tagname is a property access expression, we will then walk up to the top most of property access expression. // Then, try to get a JSX container and its associated attributes type. if parent != nil && parent.Kind == ast.KindPropertyAccessExpression { contextToken = parent parent = parent.Parent } // Fix location if parent == location { switch currentToken.Kind { case ast.KindGreaterThanToken: if parent.Kind == ast.KindJsxElement || parent.Kind == ast.KindJsxOpeningElement { location = currentToken } case ast.KindLessThanSlashToken: if parent.Kind == ast.KindJsxSelfClosingElement { location = currentToken } } } switch parent.Kind { case ast.KindJsxClosingElement: if contextToken.Kind == ast.KindLessThanSlashToken { isStartingCloseTag = true location = contextToken } case ast.KindBinaryExpression: if !binaryExpressionMayBeOpenTag(parent.AsBinaryExpression()) { break } fallthrough case ast.KindJsxSelfClosingElement, ast.KindJsxElement, ast.KindJsxOpeningElement: isJsxIdentifierExpected = true if contextToken.Kind == ast.KindLessThanToken { isRightOfOpenTag = true location = contextToken } case ast.KindJsxExpression, ast.KindJsxSpreadAttribute: // First case is for `
` or `
`, // `parent` will be `{true}` and `previousToken` will be `}`. // Second case is for `
`. // Second case must not match for `
`. if previousToken.Kind == ast.KindCloseBraceToken || previousToken.Kind == ast.KindIdentifier && previousToken.Parent.Kind == ast.KindJsxAttribute { isJsxIdentifierExpected = true } case ast.KindJsxAttribute: // For `
`, `parent` will be JsxAttribute and `previousToken` will be its initializer. if parent.Initializer() == previousToken && previousToken.End() < position { isJsxIdentifierExpected = true } else { switch previousToken.Kind { case ast.KindEqualsToken: jsxInitializer.isInitializer = true case ast.KindIdentifier: isJsxIdentifierExpected = true // For `
` we don't want to treat this as a jsx inializer, instead it's the attribute name. if parent != previousToken.Parent && parent.Initializer() == nil && findChildOfKind(parent, ast.KindEqualsToken, file) != nil { jsxInitializer.initializer = previousToken } } } } } } completionKind := CompletionKindNone hasUnresolvedAutoImports := false // This also gets mutated in nested-functions after the return var symbols []*ast.Symbol symbolToOriginInfoMap := map[ast.SymbolId]*symbolOriginInfo{} symbolToSortTextMap := map[ast.SymbolId]sortText{} var seenPropertySymbols collections.Set[ast.SymbolId] importSpecifierResolver := &importSpecifierResolverForCompletions{SourceFile: file, UserPreferences: preferences, l: l} isTypeOnlyLocation := insideJSDocTagTypeExpression || insideJsDocImportTag || importStatementCompletion != nil && ast.IsTypeOnlyImportOrExportDeclaration(location.Parent) || !isContextTokenValueLocation(contextToken) && (isPossiblyTypeArgumentPosition(contextToken, file, typeChecker) || ast.IsPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken)) addSymbolOriginInfo := func(symbol *ast.Symbol, insertQuestionDot bool, insertAwait bool) { symbolId := ast.GetSymbolId(symbol) if insertAwait && seenPropertySymbols.AddIfAbsent(symbolId) { symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindPromise, insertQuestionDot)} } else if insertQuestionDot { symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{kind: symbolOriginInfoKindNullable} } } addSymbolSortInfo := func(symbol *ast.Symbol) { symbolId := ast.GetSymbolId(symbol) if isStaticProperty(symbol) { symbolToSortTextMap[symbolId] = SortTextLocalDeclarationPriority } } addPropertySymbol := func(symbol *ast.Symbol, insertAwait bool, insertQuestionDot bool) { // For a computed property with an accessible name like `Symbol.iterator`, // we'll add a completion for the *name* `Symbol` instead of for the property. // If this is e.g. [Symbol.iterator], add a completion for `Symbol`. computedPropertyName := core.FirstNonNil(symbol.Declarations, func(decl *ast.Node) *ast.Node { name := ast.GetNameOfDeclaration(decl) if name != nil && name.Kind == ast.KindComputedPropertyName { return name } return nil }) if computedPropertyName != nil { leftMostName := getLeftMostName(computedPropertyName.Expression()) // The completion is for `Symbol`, not `iterator`. var nameSymbol *ast.Symbol if leftMostName != nil { nameSymbol = typeChecker.GetSymbolAtLocation(leftMostName) } // If this is nested like for `namespace N { export const sym = Symbol(); }`, we'll add the completion for `N`. var firstAccessibleSymbol *ast.Symbol if nameSymbol != nil { firstAccessibleSymbol = getFirstSymbolInChain(nameSymbol, contextToken, typeChecker) } var firstAccessibleSymbolId ast.SymbolId if firstAccessibleSymbol != nil { firstAccessibleSymbolId = ast.GetSymbolId(firstAccessibleSymbol) } if firstAccessibleSymbolId != 0 && seenPropertySymbols.AddIfAbsent(firstAccessibleSymbolId) { symbols = append(symbols, firstAccessibleSymbol) symbolToSortTextMap[firstAccessibleSymbolId] = SortTextGlobalsOrKeywords moduleSymbol := firstAccessibleSymbol.Parent if moduleSymbol == nil || !checker.IsExternalModuleSymbol(moduleSymbol) || typeChecker.TryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.Name, moduleSymbol) != firstAccessibleSymbol { symbolToOriginInfoMap[firstAccessibleSymbolId] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberNoExport, insertQuestionDot)} } else { var fileName string if tspath.IsExternalModuleNameRelative(stringutil.StripQuotes(moduleSymbol.Name)) { fileName = ast.GetSourceFileOfModule(moduleSymbol).FileName() } result := importSpecifierResolver.getModuleSpecifierForBestExportInfo( typeChecker, []*SymbolExportInfo{{ exportKind: ExportKindNamed, moduleFileName: fileName, isFromPackageJson: false, moduleSymbol: moduleSymbol, symbol: firstAccessibleSymbol, targetFlags: typeChecker.SkipAlias(firstAccessibleSymbol).Flags, }}, position, ast.IsValidTypeOnlyAliasUseSite(location), ) if result != nil { symbolToOriginInfoMap[ast.GetSymbolId(symbol)] = &symbolOriginInfo{ kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMemberExport, insertQuestionDot), isDefaultExport: false, fileName: fileName, data: symbolOriginInfoExport{ moduleSymbol: moduleSymbol, symbolName: firstAccessibleSymbol.Name, exportName: firstAccessibleSymbol.Name, moduleSpecifier: result.moduleSpecifier, }, } } } } else if firstAccessibleSymbolId == 0 || !seenPropertySymbols.Has(firstAccessibleSymbolId) { symbols = append(symbols, symbol) addSymbolOriginInfo(symbol, insertQuestionDot, insertAwait) addSymbolSortInfo(symbol) } } else { symbols = append(symbols, symbol) addSymbolOriginInfo(symbol, insertQuestionDot, insertAwait) addSymbolSortInfo(symbol) } } addTypeProperties := func(t *checker.Type, insertAwait bool, insertQuestionDot bool) { if typeChecker.GetStringIndexType(t) != nil { isNewIdentifierLocation = true defaultCommitCharacters = []string{} } if isRightOfQuestionDot && len(typeChecker.GetCallSignatures(t)) != 0 { isNewIdentifierLocation = true if defaultCommitCharacters == nil { defaultCommitCharacters = slices.Clone(allCommitCharacters) // Only invalid commit character here would be `(`. } } var propertyAccess *ast.Node if node.Kind == ast.KindImportType { propertyAccess = node } else { propertyAccess = node.Parent } if inCheckedFile { for _, symbol := range typeChecker.GetApparentProperties(t) { if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, t, symbol) { addPropertySymbol(symbol, false /*insertAwait*/, insertQuestionDot) } } } else { // In javascript files, for union types, we don't just get the members that // the individual types have in common, we also include all the members that // each individual type has. This is because we're going to add all identifiers // anyways. So we might as well elevate the members that were at least part // of the individual types to a higher status since we know what they are. for _, symbol := range getPropertiesForCompletion(t, typeChecker) { if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, t, symbol) { symbols = append(symbols, symbol) } } } if insertAwait { promiseType := typeChecker.GetPromisedTypeOfPromise(t) if promiseType != nil { for _, symbol := range typeChecker.GetApparentProperties(promiseType) { if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, promiseType, symbol) { addPropertySymbol(symbol, true /*insertAwait*/, insertQuestionDot) } } } } } getTypeScriptMemberSymbols := func() { // Right of dot member completion list completionKind = CompletionKindPropertyAccess // Since this is qualified name check it's a type node location isImportType := ast.IsLiteralImportTypeNode(node) isTypeLocation := (isImportType && !node.AsImportTypeNode().IsTypeOf) || ast.IsPartOfTypeNode(node.Parent) || isPossiblyTypeArgumentPosition(contextToken, file, typeChecker) isRhsOfImportDeclaration := isInRightSideOfInternalImportEqualsDeclaration(node) if ast.IsEntityName(node) || isImportType || ast.IsPropertyAccessExpression(node) { isNamespaceName := ast.IsModuleDeclaration(node.Parent) if isNamespaceName { isNewIdentifierLocation = true defaultCommitCharacters = []string{} } symbol := typeChecker.GetSymbolAtLocation(node) if symbol != nil { symbol := checker.SkipAlias(symbol, typeChecker) if symbol.Flags&(ast.SymbolFlagsModule|ast.SymbolFlagsEnum) != 0 { var valueAccessNode *ast.Node if isImportType { valueAccessNode = node } else { valueAccessNode = node.Parent } // Extract module or enum members exportedSymbols := typeChecker.GetExportsOfModule(symbol) for _, exportedSymbol := range exportedSymbols { if exportedSymbol == nil { panic("getExporsOfModule() should all be defined") } isValidValueAccess := func(s *ast.Symbol) bool { return typeChecker.IsValidPropertyAccess(valueAccessNode, s.Name) } isValidTypeAccess := func(s *ast.Symbol) bool { return symbolCanBeReferencedAtTypeLocation(s, typeChecker, collections.Set[ast.SymbolId]{}) } var isValidAccess bool if isNamespaceName { // At `namespace N.M/**/`, if this is the only declaration of `M`, don't include `M` as a completion. isValidAccess = exportedSymbol.Flags&ast.SymbolFlagsNamespace != 0 && !core.Every(exportedSymbol.Declarations, func(declaration *ast.Declaration) bool { return declaration.Parent == node.Parent }) } else if isRhsOfImportDeclaration { // Any kind is allowed when dotting off namespace in internal import equals declaration isValidAccess = isValidTypeAccess(exportedSymbol) || isValidValueAccess(exportedSymbol) } else if isTypeLocation || insideJSDocTagTypeExpression { isValidAccess = isValidTypeAccess(exportedSymbol) } else { isValidAccess = isValidValueAccess(exportedSymbol) } if isValidAccess { symbols = append(symbols, exportedSymbol) } } // If the module is merged with a value, we must get the type of the class and add its properties (for inherited static methods). if !isTypeLocation && !insideJSDocTagTypeExpression && core.Some( symbol.Declarations, func(decl *ast.Declaration) bool { return decl.Kind != ast.KindSourceFile && decl.Kind != ast.KindModuleDeclaration && decl.Kind != ast.KindEnumDeclaration }) { t := typeChecker.GetNonOptionalType(typeChecker.GetTypeOfSymbolAtLocation(symbol, node)) insertQuestionDot := false if typeChecker.IsNullableType(t) { canCorrectToQuestionDot := isRightOfDot && !isRightOfQuestionDot && !preferences.IncludeAutomaticOptionalChainCompletions.IsFalse() if canCorrectToQuestionDot || isRightOfQuestionDot { t = typeChecker.GetNonNullableType(t) if canCorrectToQuestionDot { insertQuestionDot = true } } } addTypeProperties(t, node.Flags&ast.NodeFlagsAwaitContext != 0, insertQuestionDot) } return } } } if !isTypeLocation || checker.IsInTypeQuery(node) { // microsoft/TypeScript#39946. Pulling on the type of a node inside of a function with a contextual `this` parameter can result in a circularity // if the `node` is part of the exprssion of a `yield` or `return`. This circularity doesn't exist at compile time because // we will check (and cache) the type of `this` *before* checking the type of the node. typeChecker.TryGetThisTypeAtEx(node, false /*includeGlobalThis*/, nil) t := typeChecker.GetNonOptionalType(typeChecker.GetTypeAtLocation(node)) if !isTypeLocation { insertQuestionDot := false if typeChecker.IsNullableType(t) { canCorrectToQuestionDot := isRightOfDot && !isRightOfQuestionDot && !preferences.IncludeAutomaticOptionalChainCompletions.IsFalse() if canCorrectToQuestionDot || isRightOfQuestionDot { t = typeChecker.GetNonNullableType(t) if canCorrectToQuestionDot { insertQuestionDot = true } } } addTypeProperties(t, node.Flags&ast.NodeFlagsAwaitContext != 0, insertQuestionDot) } else { addTypeProperties(typeChecker.GetNonNullableType(t), false /*insertAwait*/, false /*insertQuestionDot*/) } } } // Aggregates relevant symbols for completion in object literals in type argument positions. tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols := func() globalsSearch { typeLiteralNode := tryGetTypeLiteralNode(contextToken) if typeLiteralNode == nil { return globalsSearchContinue } intersectionTypeNode := core.IfElse( ast.IsIntersectionTypeNode(typeLiteralNode.Parent), typeLiteralNode.Parent, nil) containerTypeNode := core.IfElse( intersectionTypeNode != nil, intersectionTypeNode, typeLiteralNode) containerExpectedType := getConstraintOfTypeArgumentProperty(containerTypeNode, typeChecker) if containerExpectedType == nil { return globalsSearchContinue } containerActualType := typeChecker.GetTypeFromTypeNode(containerTypeNode) members := getPropertiesForCompletion(containerExpectedType, typeChecker) existingMembers := getPropertiesForCompletion(containerActualType, typeChecker) existingMemberNames := collections.Set[string]{} for _, member := range existingMembers { existingMemberNames.Add(member.Name) } symbols = append( symbols, core.Filter(members, func(member *ast.Symbol) bool { return !existingMemberNames.Has(member.Name) })...) completionKind = CompletionKindObjectPropertyDeclaration isNewIdentifierLocation = true return globalsSearchSuccess } // Aggregates relevant symbols for completion in object literals and object binding patterns. // Relevant symbols are stored in the captured 'symbols' variable. tryGetObjectLikeCompletionSymbols := func() globalsSearch { if contextToken != nil && contextToken.Kind == ast.KindDotDotDotToken { return globalsSearchContinue } objectLikeContainer := tryGetObjectLikeCompletionContainer(contextToken, position, file) if objectLikeContainer == nil { return globalsSearchContinue } // We're looking up possible property names from contextual/inferred/declared type. completionKind = CompletionKindObjectPropertyDeclaration var typeMembers []*ast.Symbol var existingMembers []*ast.Declaration if objectLikeContainer.Kind == ast.KindObjectLiteralExpression { instantiatedType := tryGetObjectLiteralContextualType(objectLikeContainer, typeChecker) // Check completions for Object property value shorthand if instantiatedType == nil { if objectLikeContainer.Flags&ast.NodeFlagsInWithStatement != 0 { return globalsSearchFail } return globalsSearchContinue } completionsType := typeChecker.GetContextualType(objectLikeContainer, checker.ContextFlagsCompletions) t := core.IfElse(completionsType != nil, completionsType, instantiatedType) stringIndexType := typeChecker.GetStringIndexType(t) numberIndexType := typeChecker.GetNumberIndexType(t) isNewIdentifierLocation = stringIndexType != nil || numberIndexType != nil typeMembers = getPropertiesForObjectExpression(instantiatedType, completionsType, objectLikeContainer, typeChecker) properties := objectLikeContainer.AsObjectLiteralExpression().Properties if properties != nil { existingMembers = properties.Nodes } if len(typeMembers) == 0 { // Edge case: If NumberIndexType exists if numberIndexType == nil { return globalsSearchContinue } } } else { if objectLikeContainer.Kind != ast.KindObjectBindingPattern { panic("Expected 'objectLikeContainer' to be an object binding pattern.") } // We are *only* completing on properties from the type being destructured. isNewIdentifierLocation = false rootDeclaration := ast.GetRootDeclaration(objectLikeContainer.Parent) if !ast.IsVariableLike(rootDeclaration) { panic("Root declaration is not variable-like.") } // We don't want to complete using the type acquired by the shape // of the binding pattern; we are only interested in types acquired // through type declaration or inference. // Also proceed if rootDeclaration is a parameter and if its containing function expression/arrow function is contextually typed - // type of parameter will flow in from the contextual type of the function. canGetType := ast.HasInitializer(rootDeclaration) || ast.GetTypeAnnotationNode(rootDeclaration) != nil || rootDeclaration.Parent.Parent.Kind == ast.KindForOfStatement if !canGetType && rootDeclaration.Kind == ast.KindParameter { if ast.IsExpression(rootDeclaration.Parent) { canGetType = typeChecker.GetContextualType(rootDeclaration.Parent, checker.ContextFlagsNone) != nil } else if rootDeclaration.Parent.Kind == ast.KindMethodDeclaration || rootDeclaration.Parent.Kind == ast.KindSetAccessor { canGetType = ast.IsExpression(rootDeclaration.Parent.Parent) && typeChecker.GetContextualType(rootDeclaration.Parent.Parent, checker.ContextFlagsNone) != nil } } if canGetType { typeForObject := typeChecker.GetTypeAtLocation(objectLikeContainer) if typeForObject == nil { return globalsSearchFail } typeMembers = core.Filter( typeChecker.GetPropertiesOfType(typeForObject), func(propertySymbol *ast.Symbol) bool { return typeChecker.IsPropertyAccessible( objectLikeContainer, false, /*isSuper*/ false, /*isWrite*/ typeForObject, propertySymbol, ) }, ) elements := objectLikeContainer.AsBindingPattern().Elements if elements != nil { existingMembers = elements.Nodes } } } if len(typeMembers) > 0 { // Add filtered items to the completion list. filteredMembers, spreadMemberNames := filterObjectMembersList( typeMembers, core.CheckEachDefined(existingMembers, "object like properties or elements should all be defined"), file, position, typeChecker, ) symbols = append(symbols, filteredMembers...) // Set sort texts. transformObjectLiteralMembers := preferences.IncludeCompletionsWithObjectLiteralMethodSnippets.IsTrue() && objectLikeContainer.Kind == ast.KindObjectLiteralExpression for _, member := range filteredMembers { symbolId := ast.GetSymbolId(member) if spreadMemberNames.Has(member.Name) { symbolToSortTextMap[symbolId] = SortTextMemberDeclaredBySpreadAssignment } if member.Flags&ast.SymbolFlagsOptional != 0 { _, ok := symbolToSortTextMap[symbolId] if !ok { symbolToSortTextMap[symbolId] = SortTextOptionalMember } } if transformObjectLiteralMembers { // !!! object literal member snippet completions } } } return globalsSearchSuccess } shouldOfferImportCompletions := func() bool { // If already typing an import statement, provide completions for it. if importStatementCompletion != nil { return true } // If not already a module, must have modules enabled. if !preferences.IncludeCompletionsForModuleExports.IsTrue() { return false } // Always using ES modules in 6.0+ return true } // Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` collectAutoImports := func() { if !shouldOfferImportCompletions() { return } // !!! CompletionInfoFlags // import { type | -> token text should be blank var lowerCaseTokenText string if previousToken != nil && ast.IsIdentifier(previousToken) && !(previousToken == contextToken && importStatementCompletion != nil) { lowerCaseTokenText = strings.ToLower(previousToken.Text()) } // !!! timestamp // Under `--moduleResolution nodenext` or `bundler`, we have to resolve module specifiers up front, because // package.json exports can mean we *can't* resolve a module specifier (that doesn't include a // relative path into node_modules), and we want to filter those completions out entirely. // Import statement completions always need specifier resolution because the module specifier is // part of their `insertText`, not the `codeActions` creating edits away from the cursor. // Finally, `autoImportSpecifierExcludeRegexes` necessitates eagerly resolving module specifiers // because completion items are being explcitly filtered out by module specifier. isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(location) // !!! moduleSpecifierCache := host.getModuleSpecifierCache(); // !!! packageJsonAutoImportProvider := host.getPackageJsonAutoImportProvider(); addSymbolToList := func(info []*SymbolExportInfo, symbolName string, isFromAmbientModule bool, exportMapKey ExportInfoMapKey) []*SymbolExportInfo { // Do a relatively cheap check to bail early if all re-exports are non-importable // due to file location or package.json dependency filtering. For non-node16+ // module resolution modes, getting past this point guarantees that we'll be // able to generate a suitable module specifier, so we can safely show a completion, // even if we defer computing the module specifier. info = core.Filter(info, func(i *SymbolExportInfo) bool { var toFile *ast.SourceFile if ast.IsSourceFile(i.moduleSymbol.ValueDeclaration) { toFile = i.moduleSymbol.ValueDeclaration.AsSourceFile() } return l.isImportable( file, toFile, i.moduleSymbol, preferences, importSpecifierResolver.packageJsonImportFilter(), ) }) if len(info) == 0 { return nil } // In node16+, module specifier resolution can fail due to modules being blocked // by package.json `exports`. If that happens, don't show a completion item. // N.B. We always try to resolve module specifiers here, because we have to know // now if it's going to fail so we can omit the completion from the list. result := importSpecifierResolver.getModuleSpecifierForBestExportInfo(typeChecker, info, position, isValidTypeOnlyUseSite) if result == nil { return nil } // If we skipped resolving module specifiers, our selection of which ExportInfo // to use here is arbitrary, since the info shown in the completion list derived from // it should be identical regardless of which one is used. During the subsequent // `CompletionEntryDetails` request, we'll get all the ExportInfos again and pick // the best one based on the module specifier it produces. moduleSpecifier := result.moduleSpecifier exportInfo := info[0] if result.exportInfo != nil { exportInfo = result.exportInfo } isDefaultExport := exportInfo.exportKind == ExportKindDefault if exportInfo.symbol == nil { panic("should have handled `futureExportSymbolInfo` earlier") } symbol := exportInfo.symbol if isDefaultExport { if defaultSymbol := binder.GetLocalSymbolForExportDefault(symbol); defaultSymbol != nil { symbol = defaultSymbol } } // pushAutoImportSymbol symbolId := ast.GetSymbolId(symbol) if symbolToSortTextMap[symbolId] == SortTextGlobalsOrKeywords { // If an auto-importable symbol is available as a global, don't push the auto import return nil } originInfo := &symbolOriginInfo{ kind: symbolOriginInfoKindExport, isDefaultExport: isDefaultExport, isFromPackageJson: exportInfo.isFromPackageJson, fileName: exportInfo.moduleFileName, data: &symbolOriginInfoExport{ symbolName: symbolName, moduleSymbol: exportInfo.moduleSymbol, exportName: core.IfElse(exportInfo.exportKind == ExportKindExportEquals, ast.InternalSymbolNameExportEquals, exportInfo.symbol.Name), exportMapKey: exportMapKey, moduleSpecifier: moduleSpecifier, }, } symbolToOriginInfoMap[symbolId] = originInfo symbolToSortTextMap[symbolId] = core.IfElse(importStatementCompletion != nil, SortTextLocationPriority, SortTextAutoImportSuggestions) symbols = append(symbols, symbol) return nil } l.searchExportInfosForCompletions(ctx, typeChecker, file, preferences, importStatementCompletion != nil, isRightOfOpenTag, isTypeOnlyLocation, lowerCaseTokenText, addSymbolToList, ) // !!! completionInfoFlags // !!! logging } tryGetImportCompletionSymbols := func() globalsSearch { if importStatementCompletion == nil { return globalsSearchContinue } isNewIdentifierLocation = true collectAutoImports() return globalsSearchSuccess } // Aggregates relevant symbols for completion in import clauses and export clauses // whose declarations have a module specifier; for instance, symbols will be aggregated for // // import { | } from "moduleName"; // export { a as foo, | } from "moduleName"; // // but not for // // export { | }; // // Relevant symbols are stored in the captured 'symbols' variable. tryGetImportOrExportClauseCompletionSymbols := func() globalsSearch { if contextToken == nil { return globalsSearchContinue } // `import { |` or `import { a as 0, | }` or `import { type | }` var namedImportsOrExports *ast.NamedImportsOrExports if contextToken.Kind == ast.KindOpenBraceToken || contextToken.Kind == ast.KindCommaToken { namedImportsOrExports = core.IfElse(isNamedImportsOrExports(contextToken.Parent), contextToken.Parent, nil) } else if isTypeKeywordTokenOrIdentifier(contextToken) { namedImportsOrExports = core.IfElse( isNamedImportsOrExports(contextToken.Parent.Parent), contextToken.Parent.Parent, nil, ) } if namedImportsOrExports == nil { return globalsSearchContinue } // We can at least offer `type` at `import { |` if !isTypeKeywordTokenOrIdentifier(contextToken) { keywordFilters = KeywordCompletionFiltersTypeKeyword } // try to show exported member for imported/re-exported module moduleSpecifier := core.IfElse( namedImportsOrExports.Kind == ast.KindNamedImports, namedImportsOrExports.Parent.Parent, namedImportsOrExports.Parent).ModuleSpecifier() if moduleSpecifier == nil { isNewIdentifierLocation = true if namedImportsOrExports.Kind == ast.KindNamedImports { return globalsSearchFail } return globalsSearchContinue } moduleSpecifierSymbol := typeChecker.GetSymbolAtLocation(moduleSpecifier) if moduleSpecifierSymbol == nil { isNewIdentifierLocation = true return globalsSearchFail } completionKind = CompletionKindMemberLike isNewIdentifierLocation = false exports := typeChecker.GetExportsAndPropertiesOfModule(moduleSpecifierSymbol) existing := collections.Set[string]{} for _, element := range namedImportsOrExports.Elements() { if isCurrentlyEditingNode(element, file, position) { continue } existing.Add(element.PropertyNameOrName().Text()) } uniques := core.Filter(exports, func(symbol *ast.Symbol) bool { return ast.SymbolName(symbol) != ast.InternalSymbolNameDefault && !existing.Has(ast.SymbolName(symbol)) }) symbols = append(symbols, uniques...) if len(uniques) == 0 { // If there's nothing else to import, don't offer `type` either. keywordFilters = KeywordCompletionFiltersNone } return globalsSearchSuccess } // import { x } from "foo" with { | } tryGetImportAttributesCompletionSymbols := func() globalsSearch { if contextToken == nil { return globalsSearchContinue } var importAttributes *ast.ImportAttributesNode switch contextToken.Kind { case ast.KindOpenBraceToken, ast.KindCommaToken: importAttributes = core.IfElse(ast.IsImportAttributes(contextToken.Parent), contextToken.Parent, nil) case ast.KindColonToken: importAttributes = core.IfElse(ast.IsImportAttributes(contextToken.Parent.Parent), contextToken.Parent.Parent, nil) } if importAttributes == nil { return globalsSearchContinue } var elements []*ast.Node if importAttributes.AsImportAttributes().Attributes != nil { elements = importAttributes.AsImportAttributes().Attributes.Nodes } existing := collections.NewSetFromItems(core.Map(elements, (*ast.Node).Text)...) uniques := core.Filter( typeChecker.GetApparentProperties(typeChecker.GetTypeAtLocation(importAttributes)), func(symbol *ast.Symbol) bool { return !existing.Has(ast.SymbolName(symbol)) }) symbols = append(symbols, uniques...) return globalsSearchSuccess } // Adds local declarations for completions in named exports: // export { | }; // Does not check for the absence of a module specifier (`export {} from "./other"`) // because `tryGetImportOrExportClauseCompletionSymbols` runs first and handles that, // preventing this function from running. tryGetLocalNamedExportCompletionSymbols := func() globalsSearch { if contextToken == nil { return globalsSearchContinue } var namedExports *ast.NamedExportsNode if contextToken.Kind == ast.KindOpenBraceToken || contextToken.Kind == ast.KindCommaToken { namedExports = core.IfElse(ast.IsNamedExports(contextToken.Parent), contextToken.Parent, nil) } if namedExports == nil { return globalsSearchContinue } localsContainer := ast.FindAncestor(namedExports, func(node *ast.Node) bool { return ast.IsSourceFile(node) || ast.IsModuleDeclaration(node) }) completionKind = CompletionKindNone isNewIdentifierLocation = false localSymbol := localsContainer.Symbol() var localExports ast.SymbolTable if localSymbol != nil { localExports = localSymbol.Exports } for name, symbol := range localsContainer.Locals() { symbols = append(symbols, symbol) if _, ok := localExports[name]; ok { symbolId := ast.GetSymbolId(symbol) symbolToSortTextMap[symbolId] = SortTextOptionalMember } } return globalsSearchSuccess } tryGetConstructorCompletion := func() globalsSearch { if tryGetConstructorLikeCompletionContainer(contextToken) == nil { return globalsSearchContinue } // no members, only keywords completionKind = CompletionKindNone // Declaring new property/method/accessor isNewIdentifierLocation = true // Has keywords for constructor parameter keywordFilters = KeywordCompletionFiltersConstructorParameterKeywords return globalsSearchSuccess } // Aggregates relevant symbols for completion in class declaration // Relevant symbols are stored in the captured 'symbols' variable. tryGetClassLikeCompletionSymbols := func() globalsSearch { decl := tryGetObjectTypeDeclarationCompletionContainer(file, contextToken, location, position) if decl == nil { return globalsSearchContinue } // We're looking up possible property names from parent type. completionKind = CompletionKindMemberLike // Declaring new property/method/accessor isNewIdentifierLocation = true if contextToken.Kind == ast.KindAsteriskToken { keywordFilters = KeywordCompletionFiltersNone } else if ast.IsClassLike(decl) { keywordFilters = KeywordCompletionFiltersClassElementKeywords } else { keywordFilters = KeywordCompletionFiltersInterfaceElementKeywords } // If you're in an interface you don't want to repeat things from super-interface. So just stop here. if !ast.IsClassLike(decl) { return globalsSearchSuccess } var classElement *ast.Node if contextToken.Kind == ast.KindSemicolonToken { classElement = contextToken.Parent.Parent } else { classElement = contextToken.Parent } var classElementModifierFlags ast.ModifierFlags if ast.IsClassElement(classElement) { classElementModifierFlags = classElement.ModifierFlags() } // If this is context token is not something we are editing now, consider if this would lead to be modifier. if contextToken.Kind == ast.KindIdentifier && !isCurrentlyEditingNode(contextToken, file, position) { switch contextToken.Text() { case "private": classElementModifierFlags |= ast.ModifierFlagsPrivate case "static": classElementModifierFlags |= ast.ModifierFlagsStatic case "override": classElementModifierFlags |= ast.ModifierFlagsOverride } } if ast.IsClassStaticBlockDeclaration(classElement) { classElementModifierFlags |= ast.ModifierFlagsStatic } // No member list for private methods if classElementModifierFlags&ast.ModifierFlagsPrivate == 0 { // List of property symbols of base type that are not private and already implemented var baseTypeNodes []*ast.Node if ast.IsClassLike(decl) && classElementModifierFlags&ast.ModifierFlagsOverride != 0 { baseTypeNodes = core.SingleElementSlice(ast.GetClassExtendsHeritageElement(decl)) } else { baseTypeNodes = getAllSuperTypeNodes(decl) } var baseSymbols []*ast.Symbol for _, baseTypeNode := range baseTypeNodes { t := typeChecker.GetTypeAtLocation(baseTypeNode) if classElementModifierFlags&ast.ModifierFlagsStatic != 0 { if t.Symbol() != nil { baseSymbols = append( baseSymbols, typeChecker.GetPropertiesOfType(typeChecker.GetTypeOfSymbolAtLocation(t.Symbol(), decl))...) } } else if t != nil { baseSymbols = append(baseSymbols, typeChecker.GetPropertiesOfType(t)...) } } symbols = append(symbols, filterClassMembersList(baseSymbols, decl.Members(), classElementModifierFlags, file, position)...) for _, symbol := range symbols { declaration := symbol.ValueDeclaration if declaration != nil && ast.IsClassElement(declaration) && declaration.Name() != nil && ast.IsComputedPropertyName(declaration.Name()) { symbolId := ast.GetSymbolId(symbol) origin := &symbolOriginInfo{ kind: symbolOriginInfoKindComputedPropertyName, data: &symbolOriginInfoComputedPropertyName{symbolName: typeChecker.SymbolToString(symbol)}, } symbolToOriginInfoMap[symbolId] = origin } } } return globalsSearchSuccess } tryGetJsxCompletionSymbols := func() globalsSearch { jsxContainer := tryGetContainingJsxElement(contextToken, file) if jsxContainer == nil { return globalsSearchContinue } // Cursor is inside a JSX self-closing element or opening element. attrsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsNone) if attrsType == nil { return globalsSearchContinue } completionsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsCompletions) filteredSymbols, spreadMemberNames := filterJsxAttributes( getPropertiesForObjectExpression(attrsType, completionsType, jsxContainer.Attributes(), typeChecker), jsxContainer.Attributes().Properties(), file, position, typeChecker, ) symbols = append(symbols, filteredSymbols...) // Set sort texts. for _, symbol := range filteredSymbols { symbolId := ast.GetSymbolId(symbol) if spreadMemberNames.Has(ast.SymbolName(symbol)) { symbolToSortTextMap[symbolId] = SortTextMemberDeclaredBySpreadAssignment } if symbol.Flags&ast.SymbolFlagsOptional != 0 { _, ok := symbolToSortTextMap[symbolId] if !ok { symbolToSortTextMap[symbolId] = SortTextOptionalMember } } } completionKind = CompletionKindMemberLike isNewIdentifierLocation = false return globalsSearchSuccess } getGlobalCompletions := func() globalsSearch { if tryGetFunctionLikeBodyCompletionContainer(contextToken) != nil { keywordFilters = KeywordCompletionFiltersFunctionLikeBodyKeywords } else { keywordFilters = KeywordCompletionFiltersAll } // Get all entities in the current scope. completionKind = CompletionKindGlobal isNewIdentifierLocation, defaultCommitCharacters = computeCommitCharactersAndIsNewIdentifier(contextToken, file, position) if previousToken != contextToken { if previousToken == nil { panic("Expected 'contextToken' to be defined when different from 'previousToken'.") } } // We need to find the node that will give us an appropriate scope to begin // aggregating completion candidates. This is achieved in 'getScopeNode' // by finding the first node that encompasses a position, accounting for whether a node // is "complete" to decide whether a position belongs to the node. // // However, at the end of an identifier, we are interested in the scope of the identifier // itself, but fall outside of the identifier. For instance: // // xyz => x$ // // the cursor is outside of both the 'x' and the arrow function 'xyz => x', // so 'xyz' is not returned in our results. // // We define 'adjustedPosition' so that we may appropriately account for // being at the end of an identifier. The intention is that if requesting completion // at the end of an identifier, it should be effectively equivalent to requesting completion // anywhere inside/at the beginning of the identifier. So in the previous case, the // 'adjustedPosition' will work as if requesting completion in the following: // // xyz => $x // // If previousToken !== contextToken, then // - 'contextToken' was adjusted to the token prior to 'previousToken' // because we were at the end of an identifier. // - 'previousToken' is defined. var adjustedPosition int if previousToken != contextToken { adjustedPosition = astnav.GetStartOfNode(previousToken, file, false /*includeJSDoc*/) } else { adjustedPosition = position } scopeNode := getScopeNode(contextToken, adjustedPosition, file) if scopeNode == nil { scopeNode = file.AsNode() } isInSnippetScope = isSnippetScope(scopeNode) symbolMeanings := core.IfElse(isTypeOnlyLocation, ast.SymbolFlagsNone, ast.SymbolFlagsValue) | ast.SymbolFlagsType | ast.SymbolFlagsNamespace | ast.SymbolFlagsAlias typeOnlyAliasNeedsPromotion := previousToken != nil && !ast.IsValidTypeOnlyAliasUseSite(previousToken) symbols = append(symbols, typeChecker.GetSymbolsInScope(scopeNode, symbolMeanings)...) core.CheckEachDefined(symbols, "getSymbolsInScope() should all be defined") for _, symbol := range symbols { symbolId := ast.GetSymbolId(symbol) if !typeChecker.IsArgumentsSymbol(symbol) && !core.Some(symbol.Declarations, func(decl *ast.Declaration) bool { return ast.GetSourceFileOfNode(decl) == file }) { symbolToSortTextMap[symbolId] = SortTextGlobalsOrKeywords } if typeOnlyAliasNeedsPromotion && symbol.Flags&ast.SymbolFlagsValue == 0 { typeOnlyAliasDeclaration := core.Find(symbol.Declarations, ast.IsTypeOnlyImportDeclaration) if typeOnlyAliasDeclaration != nil { origin := &symbolOriginInfo{ kind: symbolOriginInfoKindTypeOnlyAlias, data: &symbolOriginInfoTypeOnlyAlias{declaration: typeOnlyAliasDeclaration}, } symbolToOriginInfoMap[symbolId] = origin } } } // Need to insert 'this.' before properties of `this` type. if scopeNode.Kind != ast.KindSourceFile { thisType := typeChecker.TryGetThisTypeAtEx( scopeNode, false, /*includeGlobalThis*/ core.IfElse(ast.IsClassLike(scopeNode.Parent), scopeNode, nil)) if thisType != nil && !isProbablyGlobalType(thisType, file, typeChecker) { for _, symbol := range getPropertiesForCompletion(thisType, typeChecker) { symbolId := ast.GetSymbolId(symbol) symbols = append(symbols, symbol) symbolToOriginInfoMap[symbolId] = &symbolOriginInfo{kind: symbolOriginInfoKindThisType} symbolToSortTextMap[symbolId] = SortTextSuggestedClassMembers } } } collectAutoImports() if isTypeOnlyLocation { if contextToken != nil && ast.IsAssertionExpression(contextToken.Parent) { keywordFilters = KeywordCompletionFiltersTypeAssertionKeywords } else { keywordFilters = KeywordCompletionFiltersTypeKeywords } } return globalsSearchSuccess } tryGetGlobalSymbols := func() bool { var result globalsSearch globalSearchFuncs := []func() globalsSearch{ tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols, tryGetObjectLikeCompletionSymbols, tryGetImportCompletionSymbols, tryGetImportOrExportClauseCompletionSymbols, tryGetImportAttributesCompletionSymbols, tryGetLocalNamedExportCompletionSymbols, tryGetConstructorCompletion, tryGetClassLikeCompletionSymbols, tryGetJsxCompletionSymbols, getGlobalCompletions, } for _, globalSearchFunc := range globalSearchFuncs { result = globalSearchFunc() if result != globalsSearchContinue { break } } return result == globalsSearchSuccess } if isRightOfDot || isRightOfQuestionDot { getTypeScriptMemberSymbols() } else if isRightOfOpenTag { symbols = typeChecker.GetJsxIntrinsicTagNamesAt(location) core.CheckEachDefined(symbols, "GetJsxIntrinsicTagNamesAt() should all be defined") tryGetGlobalSymbols() completionKind = CompletionKindGlobal keywordFilters = KeywordCompletionFiltersNone } else if isStartingCloseTag { tagName := contextToken.Parent.Parent.AsJsxElement().OpeningElement.TagName() tagSymbol := typeChecker.GetSymbolAtLocation(tagName) if tagSymbol != nil { symbols = []*ast.Symbol{tagSymbol} } completionKind = CompletionKindGlobal keywordFilters = KeywordCompletionFiltersNone } else { // For JavaScript or TypeScript, if we're not after a dot, then just try to get the // global symbols in scope. These results should be valid for either language as // the set of symbols that can be referenced from this location. if !tryGetGlobalSymbols() { if keywordFilters != KeywordCompletionFiltersNone { return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation) } return nil } } var contextualType *checker.Type if previousToken != nil { contextualType = getContextualType(previousToken, position, file, typeChecker) } // exclude literal suggestions after microsoft/TypeScript#51667) and after closing quote (microsoft/TypeScript#52675) // for strings getStringLiteralCompletions handles completions isLiteralExpected := !(previousToken != nil && ast.IsStringLiteralLike(previousToken)) && !isJsxIdentifierExpected var literals []literalValue if isLiteralExpected { var types []*checker.Type if contextualType != nil && contextualType.IsUnion() { types = contextualType.Types() } else if contextualType != nil { types = []*checker.Type{contextualType} } literals = core.MapNonNil(types, func(t *checker.Type) literalValue { if isLiteral(t) && !t.IsEnumLiteral() { return t.AsLiteralType().Value() } return nil }) } var recommendedCompletion *ast.Symbol if previousToken != nil && contextualType != nil { recommendedCompletion = getRecommendedCompletion(previousToken, contextualType, typeChecker) } if defaultCommitCharacters == nil { defaultCommitCharacters = getDefaultCommitCharacters(isNewIdentifierLocation) } return &completionDataData{ symbols: symbols, completionKind: completionKind, isInSnippetScope: isInSnippetScope, propertyAccessToConvert: propertyAccessToConvert, isNewIdentifierLocation: isNewIdentifierLocation, location: location, keywordFilters: keywordFilters, literals: literals, symbolToOriginInfoMap: symbolToOriginInfoMap, symbolToSortTextMap: symbolToSortTextMap, recommendedCompletion: recommendedCompletion, previousToken: previousToken, contextToken: contextToken, jsxInitializer: jsxInitializer, insideJSDocTagTypeExpression: insideJSDocTagTypeExpression, isTypeOnlyLocation: isTypeOnlyLocation, isJsxIdentifierExpected: isJsxIdentifierExpected, isRightOfOpenTag: isRightOfOpenTag, isRightOfDotOrQuestionDot: isRightOfDot || isRightOfQuestionDot, importStatementCompletion: importStatementCompletion, hasUnresolvedAutoImports: hasUnresolvedAutoImports, defaultCommitCharacters: defaultCommitCharacters, } } func keywordCompletionData( keywordFilters KeywordCompletionFilters, filterOutTSOnlyKeywords bool, isNewIdentifierLocation bool, ) *completionDataKeyword { return &completionDataKeyword{ keywordCompletions: getKeywordCompletions(keywordFilters, filterOutTSOnlyKeywords), isNewIdentifierLocation: isNewIdentifierLocation, } } func getDefaultCommitCharacters(isNewIdentifierLocation bool) []string { if isNewIdentifierLocation { return []string{} } return slices.Clone(allCommitCharacters) } func (l *LanguageService) completionInfoFromData( ctx context.Context, typeChecker *checker.Checker, file *ast.SourceFile, compilerOptions *core.CompilerOptions, data *completionDataData, preferences *UserPreferences, position int, clientOptions *lsproto.CompletionClientCapabilities, optionalReplacementSpan *lsproto.Range, ) *lsproto.CompletionList { keywordFilters := data.keywordFilters isNewIdentifierLocation := data.isNewIdentifierLocation contextToken := data.contextToken literals := data.literals // Verify if the file is JSX language variant if file.LanguageVariant == core.LanguageVariantJSX { list := l.getJsxClosingTagCompletion(data.location, file, position, clientOptions) if list != nil { return list } } // When the completion is for the expression of a case clause (e.g. `case |`), // filter literals & enum symbols whose values are already present in existing case clauses. caseClause := ast.FindAncestor(contextToken, ast.IsCaseClause) if caseClause != nil && (contextToken.Kind == ast.KindCaseKeyword || ast.IsNodeDescendantOf(contextToken, caseClause.Expression())) { tracker := newCaseClauseTracker(typeChecker, caseClause.Parent.AsCaseBlock().Clauses.Nodes) literals = core.Filter(literals, func(literal literalValue) bool { return !tracker.hasValue(literal) }) data.symbols = core.Filter(data.symbols, func(symbol *ast.Symbol) bool { if symbol.ValueDeclaration != nil && ast.IsEnumMember(symbol.ValueDeclaration) { value := typeChecker.GetConstantValue(symbol.ValueDeclaration) if value != nil && tracker.hasValue(value) { return false } } return true }) } isChecked := isCheckedFile(file, compilerOptions) if isChecked && !isNewIdentifierLocation && len(data.symbols) == 0 && keywordFilters == KeywordCompletionFiltersNone { return nil } uniqueNames, sortedEntries := l.getCompletionEntriesFromSymbols( ctx, data, nil, /*replacementToken*/ position, file, preferences, compilerOptions, clientOptions, ) if data.keywordFilters != KeywordCompletionFiltersNone { keywordCompletions := getKeywordCompletions(data.keywordFilters, !data.insideJSDocTagTypeExpression && ast.IsSourceFileJS(file)) for _, keywordEntry := range keywordCompletions { if data.isTypeOnlyLocation && isTypeKeyword(scanner.StringToToken(keywordEntry.Label)) || !data.isTypeOnlyLocation && isContextualKeywordInAutoImportableExpressionSpace(keywordEntry.Label) || !uniqueNames.Has(keywordEntry.Label) { uniqueNames.Add(keywordEntry.Label) sortedEntries = core.InsertSorted(sortedEntries, keywordEntry, compareCompletionEntries) } } } for _, keywordEntry := range getContextualKeywords(file, contextToken, position) { if !uniqueNames.Has(keywordEntry.Label) { uniqueNames.Add(keywordEntry.Label) sortedEntries = core.InsertSorted(sortedEntries, keywordEntry, compareCompletionEntries) } } for _, literal := range literals { literalEntry := createCompletionItemForLiteral(file, preferences, literal) uniqueNames.Add(literalEntry.Label) sortedEntries = core.InsertSorted(sortedEntries, literalEntry, compareCompletionEntries) } if !isChecked { sortedEntries = l.getJSCompletionEntries( ctx, file, position, &uniqueNames, sortedEntries, ) } // !!! exhaustive case completions itemDefaults := l.setItemDefaults( clientOptions, position, file, sortedEntries, &data.defaultCommitCharacters, optionalReplacementSpan, ) return &lsproto.CompletionList{ IsIncomplete: data.hasUnresolvedAutoImports, ItemDefaults: itemDefaults, Items: sortedEntries, } } func (l *LanguageService) getCompletionEntriesFromSymbols( ctx context.Context, data *completionDataData, replacementToken *ast.Node, position int, file *ast.SourceFile, preferences *UserPreferences, compilerOptions *core.CompilerOptions, clientOptions *lsproto.CompletionClientCapabilities, ) (uniqueNames collections.Set[string], sortedEntries []*lsproto.CompletionItem) { closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location) useSemicolons := probablyUsesSemicolons(file) typeChecker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) defer done() isMemberCompletion := isMemberCompletionKind(data.completionKind) // Tracks unique names. // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; // true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports. // So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name. uniques := make(uniqueNamesMap) for _, symbol := range data.symbols { symbolId := ast.GetSymbolId(symbol) origin := data.symbolToOriginInfoMap[symbolId] name, needsConvertPropertyAccess := getCompletionEntryDisplayNameForSymbol( symbol, origin, data.completionKind, data.isJsxIdentifierExpected, ) if name == "" || uniques[name] && (origin == nil || !originIsObjectLiteralMethod(origin)) || data.completionKind == CompletionKindGlobal && !shouldIncludeSymbol(symbol, data, closestSymbolDeclaration, file, typeChecker, compilerOptions) { continue } // When in a value location in a JS file, ignore symbols that definitely seem to be type-only. if !data.isTypeOnlyLocation && ast.IsSourceFileJS(file) && symbolAppearsToBeTypeOnly(symbol, typeChecker) { continue } originalSortText := data.symbolToSortTextMap[ast.GetSymbolId(symbol)] if originalSortText == "" { originalSortText = SortTextLocationPriority } var sortText sortText if isDeprecated(symbol, typeChecker) { sortText = DeprecateSortText(originalSortText) } else { sortText = originalSortText } entry := l.createCompletionItem( ctx, typeChecker, symbol, sortText, replacementToken, data, position, file, name, needsConvertPropertyAccess, origin, useSemicolons, compilerOptions, preferences, clientOptions, isMemberCompletion, ) if entry == nil { continue } // True for locals; false for globals, module exports from other files, `this.` completions. shouldShadowLaterSymbols := (origin == nil || originIsTypeOnlyAlias(origin)) && !(symbol.Parent == nil && !core.Some(symbol.Declarations, func(d *ast.Node) bool { return ast.GetSourceFileOfNode(d) == file })) uniques[name] = shouldShadowLaterSymbols sortedEntries = core.InsertSorted(sortedEntries, entry, compareCompletionEntries) } uniqueSet := collections.NewSetWithSizeHint[string](len(uniques)) for name := range maps.Keys(uniques) { uniqueSet.Add(name) } return *uniqueSet, sortedEntries } func completionNameForLiteral( file *ast.SourceFile, preferences *UserPreferences, literal literalValue, ) string { switch literal := literal.(type) { case string: return quote(file, preferences, literal) case jsnum.Number: name, _ := core.StringifyJson(literal, "" /*prefix*/, "" /*suffix*/) return name case jsnum.PseudoBigInt: return literal.String() + "n" } panic(fmt.Sprintf("Unhandled literal value: %v", literal)) } func createCompletionItemForLiteral( file *ast.SourceFile, preferences *UserPreferences, literal literalValue, ) *lsproto.CompletionItem { return &lsproto.CompletionItem{ Label: completionNameForLiteral(file, preferences, literal), Kind: ptrTo(lsproto.CompletionItemKindConstant), SortText: ptrTo(string(SortTextLocationPriority)), CommitCharacters: ptrTo([]string{}), } } func (l *LanguageService) createCompletionItem( ctx context.Context, typeChecker *checker.Checker, symbol *ast.Symbol, sortText sortText, replacementToken *ast.Node, data *completionDataData, position int, file *ast.SourceFile, name string, needsConvertPropertyAccess bool, origin *symbolOriginInfo, useSemicolons bool, compilerOptions *core.CompilerOptions, preferences *UserPreferences, clientOptions *lsproto.CompletionClientCapabilities, isMemberCompletion bool, ) *lsproto.CompletionItem { contextToken := data.contextToken var insertText string var filterText string replacementSpan := l.getReplacementRangeForContextToken(file, replacementToken, position) var isSnippet, hasAction bool source := getSourceFromOrigin(origin) var labelDetails *lsproto.CompletionItemLabelDetails insertQuestionDot := originIsNullableMember(origin) useBraces := originIsSymbolMember(origin) || needsConvertPropertyAccess if originIsThisType(origin) { if needsConvertPropertyAccess { insertText = fmt.Sprintf( "this%s[%s]", core.IfElse(insertQuestionDot, "?.", ""), quotePropertyName(file, preferences, name)) } else { insertText = fmt.Sprintf( "this%s%s", core.IfElse(insertQuestionDot, "?.", "."), name) } } else if data.propertyAccessToConvert != nil && (useBraces || insertQuestionDot) { // We should only have needsConvertPropertyAccess if there's a property access to convert. But see microsoft/TypeScript#21790. // Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro. if useBraces { if needsConvertPropertyAccess { insertText = fmt.Sprintf("[%s]", quotePropertyName(file, preferences, name)) } else { insertText = fmt.Sprintf("[%s]", name) } } else { insertText = name } if insertQuestionDot || data.propertyAccessToConvert.AsPropertyAccessExpression().QuestionDotToken != nil { insertText = "?." + insertText } dot := findChildOfKind(data.propertyAccessToConvert, ast.KindDotToken, file) if dot == nil { dot = findChildOfKind(data.propertyAccessToConvert, ast.KindQuestionDotToken, file) } if dot == nil { return nil } // If the text after the '.' starts with this name, write over it. Else, add new text. var end int if strings.HasPrefix(name, data.propertyAccessToConvert.Name().Text()) { end = data.propertyAccessToConvert.End() } else { end = dot.End() } replacementSpan = l.createLspRangeFromBounds(astnav.GetStartOfNode(dot, file, false /*includeJSDoc*/), end, file) } if data.jsxInitializer.isInitializer { if insertText == "" { insertText = name } insertText = fmt.Sprintf("{%s}", insertText) if data.jsxInitializer.initializer != nil { replacementSpan = l.createLspRangeFromNode(data.jsxInitializer.initializer, file) } } if originIsPromise(origin) && data.propertyAccessToConvert != nil { if insertText == "" { insertText = name } precedingToken := astnav.FindPrecedingToken(file, data.propertyAccessToConvert.Pos()) var awaitText string if precedingToken != nil && lsutil.PositionIsASICandidate(precedingToken.End(), precedingToken.Parent, file) { awaitText = ";" } awaitText += "(await " + scanner.GetTextOfNode(data.propertyAccessToConvert.Expression()) + ")" if needsConvertPropertyAccess { insertText = awaitText + insertText } else { dotStr := core.IfElse(insertQuestionDot, "?.", ".") insertText = awaitText + dotStr + insertText } isInAwaitExpression := ast.IsAwaitExpression(data.propertyAccessToConvert.Parent) wrapNode := core.IfElse( isInAwaitExpression, data.propertyAccessToConvert.Parent, data.propertyAccessToConvert.Expression(), ) replacementSpan = l.createLspRangeFromBounds( astnav.GetStartOfNode(wrapNode, file, false /*includeJSDoc*/), data.propertyAccessToConvert.End(), file) } if originIsExport(origin) { resolvedOrigin := origin.asExport() labelDetails = &lsproto.CompletionItemLabelDetails{ Description: &resolvedOrigin.moduleSpecifier, // !!! vscode @link support } if data.importStatementCompletion != nil { quotedModuleSpecifier := escapeSnippetText(quote(file, preferences, resolvedOrigin.moduleSpecifier)) exportKind := ExportKindNamed if origin.isDefaultExport { exportKind = ExportKindDefault } else if resolvedOrigin.exportName == ast.InternalSymbolNameExportEquals { exportKind = ExportKindExportEquals } insertText = "import " typeOnlyText := scanner.TokenToString(ast.KindTypeKeyword) + " " if data.importStatementCompletion.isTopLevelTypeOnly { insertText += typeOnlyText } tabStop := core.IfElse(ptrIsTrue(clientOptions.CompletionItem.SnippetSupport), "$1", "") importKind := getImportKind(file, exportKind, l.GetProgram(), true /*forceImportKeyword*/) escapedSnippet := escapeSnippetText(name) suffix := core.IfElse(useSemicolons, ";", "") switch importKind { case ImportKindCommonJS: insertText += fmt.Sprintf(`%s%s = require(%s)%s`, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) case ImportKindDefault: insertText += fmt.Sprintf(`%s%s from %s%s`, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) case ImportKindNamespace: insertText += fmt.Sprintf(`* as %s from %s%s`, escapedSnippet, quotedModuleSpecifier, suffix) case ImportKindNamed: importSpecifierTypeOnlyText := core.IfElse(data.importStatementCompletion.couldBeTypeOnlyImportSpecifier, typeOnlyText, "") insertText += fmt.Sprintf(`{ %s%s%s } from %s%s`, importSpecifierTypeOnlyText, escapedSnippet, tabStop, quotedModuleSpecifier, suffix) } replacementSpan = data.importStatementCompletion.replacementSpan isSnippet = ptrIsTrue(clientOptions.CompletionItem.SnippetSupport) } } if originIsTypeOnlyAlias(origin) { hasAction = true } // Provide object member completions when missing commas, and insert missing commas. // For example: // // interface I { // a: string; // b: number // } // // const cc: I = { a: "red" | } // // Completion should add a comma after "red" and provide completions for b if data.completionKind == CompletionKindObjectPropertyDeclaration && contextToken != nil && !ast.NodeHasKind(astnav.FindPrecedingTokenEx(file, contextToken.Pos(), contextToken, false /*excludeJSDoc*/), ast.KindCommaToken) { if ast.IsMethodDeclaration(contextToken.Parent.Parent) || ast.IsGetAccessorDeclaration(contextToken.Parent.Parent) || ast.IsSetAccessorDeclaration(contextToken.Parent.Parent) || ast.IsSpreadAssignment(contextToken.Parent) || lsutil.GetLastToken(ast.FindAncestor(contextToken.Parent, ast.IsPropertyAssignment), file) == contextToken || ast.IsShorthandPropertyAssignment(contextToken.Parent) && getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position) { source = string(completionSourceObjectLiteralMemberWithComma) hasAction = true } } if preferences.IncludeCompletionsWithClassMemberSnippets.IsTrue() && data.completionKind == CompletionKindMemberLike && isClassLikeMemberCompletion(symbol, data.location, file) { // !!! class member completions } if originIsObjectLiteralMethod(origin) { insertText = origin.asObjectLiteralMethod().insertText isSnippet = origin.asObjectLiteralMethod().isSnippet labelDetails = origin.asObjectLiteralMethod().labelDetails // !!! check if this can conflict with case above where we set label details if !clientSupportsItemLabelDetails(clientOptions) { name = name + *origin.asObjectLiteralMethod().labelDetails.Detail labelDetails = nil } source = string(completionSourceObjectLiteralMethodSnippet) sortText = sortBelow(sortText) } if data.isJsxIdentifierExpected && !data.isRightOfOpenTag && clientSupportsItemSnippet(clientOptions) && preferences.JsxAttributeCompletionStyle != JsxAttributeCompletionStyleNone && !(ast.IsJsxAttribute(data.location.Parent) && data.location.Parent.Initializer() != nil) { useBraces := preferences.JsxAttributeCompletionStyle == JsxAttributeCompletionStyleBraces t := typeChecker.GetTypeOfSymbolAtLocation(symbol, data.location) // If is boolean like or undefined, don't return a snippet, we want to return just the completion. if preferences.JsxAttributeCompletionStyle == JsxAttributeCompletionStyleAuto && !t.IsBooleanLike() && !(t.IsUnion() && core.Some(t.Types(), (*checker.Type).IsBooleanLike)) { if t.IsStringLike() || t.IsUnion() && core.Every( t.Types(), func(t *checker.Type) bool { return t.Flags()&(checker.TypeFlagsStringLike|checker.TypeFlagsUndefined) != 0 || isStringAndEmptyAnonymousObjectIntersection(typeChecker, t) }) { // If type is string-like or undefined, use quotes. insertText = fmt.Sprintf("%s=%s", escapeSnippetText(name), quote(file, preferences, "$1")) isSnippet = true } else { // Use braces for everything else. useBraces = true } } if useBraces { insertText = escapeSnippetText(name) + "={$1}" isSnippet = true } } var autoImportData *completionEntryData if originIsExport(origin) { autoImportData = origin.toCompletionEntryData() hasAction = data.importStatementCompletion == nil } parentNamedImportOrExport := ast.FindAncestor(data.location, isNamedImportsOrExports) if parentNamedImportOrExport != nil { if !scanner.IsIdentifierText(name, core.LanguageVariantStandard) { insertText = quotePropertyName(file, preferences, name) if parentNamedImportOrExport.Kind == ast.KindNamedImports { // Check if it is `import { ^here as name } from '...'``. // We have to access the scanner here to check if it is `{ ^here as name }`` or `{ ^here, as, name }`. scanner := scanner.NewScanner() scanner.SetText(file.Text()) scanner.ResetPos(position) if !(scanner.Scan() == ast.KindAsKeyword && scanner.Scan() == ast.KindIdentifier) { insertText += " as " + generateIdentifierForArbitraryString(name) } } } else if parentNamedImportOrExport.Kind == ast.KindNamedImports { possibleToken := scanner.StringToToken(name) if possibleToken != ast.KindUnknown && (possibleToken == ast.KindAwaitKeyword || isNonContextualKeyword(possibleToken)) { insertText = fmt.Sprintf("%s as %s_", name, name) } } } // Commit characters elementKind := getSymbolKind(typeChecker, symbol, data.location) var commitCharacters *[]string if clientSupportsItemCommitCharacters(clientOptions) { if elementKind == ScriptElementKindWarning || elementKind == ScriptElementKindString { commitCharacters = &[]string{} } else if !clientSupportsDefaultCommitCharacters(clientOptions) { commitCharacters = ptrTo(data.defaultCommitCharacters) } // Otherwise use the completion list default. } preselect := isRecommendedCompletionMatch(symbol, data.recommendedCompletion, typeChecker) kindModifiers := getSymbolModifiers(typeChecker, symbol) return l.createLSPCompletionItem( name, insertText, filterText, sortText, elementKind, kindModifiers, replacementSpan, commitCharacters, labelDetails, file, position, clientOptions, isMemberCompletion, isSnippet, hasAction, preselect, source, autoImportData, ) } func isRecommendedCompletionMatch(localSymbol *ast.Symbol, recommendedCompletion *ast.Symbol, typeChecker *checker.Checker) bool { return localSymbol == recommendedCompletion || localSymbol.Flags&ast.SymbolFlagsExportValue != 0 && typeChecker.GetExportSymbolOfSymbol(localSymbol) == recommendedCompletion } // Ported from vscode. var wordSeparators = collections.NewSetFromItems( '`', '~', '!', '@', '%', '^', '&', '*', '(', ')', '-', '=', '+', '[', '{', ']', '}', '\\', '|', ';', ':', '\'', '"', ',', '.', '<', '>', '/', '?', ) // Finds the length and first rune of the word that ends at the given position. // e.g. for "abc def.ghi|jkl", the word length is 3 and the word start is 'g'. func getWordLengthAndStart(sourceFile *ast.SourceFile, position int) (wordLength int, wordStart rune) { // !!! Port other case of vscode's `DEFAULT_WORD_REGEXP` that covers words that start like numbers, e.g. -123.456abcd. text := sourceFile.Text()[:position] totalSize := 0 var firstRune rune for r, size := utf8.DecodeLastRuneInString(text); size != 0; r, size = utf8.DecodeLastRuneInString(text[:len(text)-totalSize]) { if wordSeparators.Has(r) || unicode.IsSpace(r) { break } totalSize += size firstRune = r } // If word starts with `@`, disregard this first character. if firstRune == '@' { totalSize -= 1 firstRune, _ = utf8.DecodeRuneInString(text[len(text)-totalSize:]) } return totalSize, firstRune } // `["ab c"]` -> `ab c` // `['ab c']` -> `ab c` // `[123]` -> `123` func trimElementAccess(text string) string { text = strings.TrimPrefix(text, "[") text = strings.TrimSuffix(text, "]") if strings.HasPrefix(text, `'`) && strings.HasSuffix(text, `'`) { text = strings.TrimPrefix(strings.TrimSuffix(text, `'`), `'`) } if strings.HasPrefix(text, `"`) && strings.HasSuffix(text, `"`) { text = strings.TrimPrefix(strings.TrimSuffix(text, `"`), `"`) } return text } // Ported from vscode ts extension: `getFilterText`. func getFilterText( file *ast.SourceFile, position int, insertText string, label string, wordStart rune, dotAccessor string, ) string { // Private field completion, e.g. label `#bar`. if strings.HasPrefix(label, "#") { if insertText != "" { if strings.HasPrefix(insertText, "this.#") { if wordStart == '#' { // `method() { this.#| }` // `method() { #| }` return "" } else { // `method() { this.| }` // `method() { | }` return strings.TrimPrefix(insertText, "this.#") } } } else { if wordStart == '#' { // `method() { this.#| }` return "" } else { // `method() { this.| }` // `method() { | }` return strings.TrimPrefix(label, "#") } } } // For `this.` completions, generally don't set the filter text since we don't want them to be overly deprioritized. microsoft/vscode#74164 if strings.HasPrefix(insertText, "this.") { return "" } // Handle the case: // ``` // const xyz = { 'ab c': 1 }; // xyz.ab| // ``` // In which case we want to insert a bracket accessor but should use `.abc` as the filter text instead of // the bracketed insert text. if strings.HasPrefix(insertText, "[") { return dotAccessor + trimElementAccess(insertText) } if strings.HasPrefix(insertText, "?.") { // Handle this case like the case above: // ``` // const xyz = { 'ab c': 1 } | undefined; // xyz.ab| // ``` // filterText should be `.ab c` instead of `?.['ab c']`. if strings.HasPrefix(insertText, "?.[") { return dotAccessor + trimElementAccess(insertText[2:]) } else { // ``` // const xyz = { abc: 1 } | undefined; // xyz.ab| // ``` // filterText should be `.abc` instead of `?.abc. return dotAccessor + insertText[2:] } } // In all other cases, fall back to using the insertText. return insertText } // Ported from vscode's `provideCompletionItems`. func getDotAccessor(file *ast.SourceFile, position int) string { text := file.Text()[:position] totalSize := 0 if strings.HasSuffix(text, "?.") { totalSize += 2 return file.Text()[position-totalSize : position] } if strings.HasSuffix(text, ".") { totalSize += 1 return file.Text()[position-totalSize : position] } return "" } func strPtrIsEmpty(ptr *string) bool { if ptr == nil { return true } return *ptr == "" } func strPtrTo(v string) *string { if v == "" { return nil } return &v } func ptrIsTrue(ptr *bool) bool { if ptr == nil { return false } return *ptr } func ptrIsFalse(ptr *bool) bool { if ptr == nil { return false } return !*ptr } func boolToPtr(v bool) *bool { if v { return ptrTo(true) } return nil } func getLineOfPosition(file *ast.SourceFile, pos int) int { line, _ := scanner.GetECMALineAndCharacterOfPosition(file, pos) return line } func getLineEndOfPosition(file *ast.SourceFile, pos int) int { line := getLineOfPosition(file, pos) lineStarts := scanner.GetECMALineStarts(file) var lastCharPos int if line+1 >= len(lineStarts) { lastCharPos = file.End() } else { lastCharPos = int(lineStarts[line+1]) - 1 } fullText := file.Text() if lastCharPos > 0 && lastCharPos < len(fullText) && fullText[lastCharPos] == '\n' && fullText[lastCharPos-1] == '\r' { return lastCharPos - 1 } return lastCharPos } func isClassLikeMemberCompletion(symbol *ast.Symbol, location *ast.Node, file *ast.SourceFile) bool { // !!! class member completions return false } func symbolAppearsToBeTypeOnly(symbol *ast.Symbol, typeChecker *checker.Checker) bool { flags := checker.GetCombinedLocalAndExportSymbolFlags(checker.SkipAlias(symbol, typeChecker)) return flags&ast.SymbolFlagsValue == 0 && (len(symbol.Declarations) == 0 || !ast.IsInJSFile(symbol.Declarations[0]) || flags&ast.SymbolFlagsType != 0) } func shouldIncludeSymbol( symbol *ast.Symbol, data *completionDataData, closestSymbolDeclaration *ast.Declaration, file *ast.SourceFile, typeChecker *checker.Checker, compilerOptions *core.CompilerOptions, ) bool { allFlags := symbol.Flags location := data.location // export = /**/ here we want to get all meanings, so any symbol is ok if location.Parent != nil && ast.IsExportAssignment(location.Parent) { return true } // Filter out variables from their own initializers // `const a = /* no 'a' here */` if closestSymbolDeclaration != nil && ast.IsVariableDeclaration(closestSymbolDeclaration) && symbol.ValueDeclaration == closestSymbolDeclaration { return false } // Filter out current and latter parameters from defaults // `function f(a = /* no 'a' and 'b' here */, b) { }` or // `function f(a: T, b: T2) { }` var symbolDeclaration *ast.Declaration if symbol.ValueDeclaration != nil { symbolDeclaration = symbol.ValueDeclaration } else if len(symbol.Declarations) > 0 { symbolDeclaration = symbol.Declarations[0] } if closestSymbolDeclaration != nil && symbolDeclaration != nil { if ast.IsParameter(closestSymbolDeclaration) && ast.IsParameter(symbolDeclaration) { parameters := closestSymbolDeclaration.Parent.ParameterList() if symbolDeclaration.Pos() >= closestSymbolDeclaration.Pos() && symbolDeclaration.Pos() < parameters.End() { return false } } else if ast.IsTypeParameterDeclaration(closestSymbolDeclaration) && ast.IsTypeParameterDeclaration(symbolDeclaration) { if closestSymbolDeclaration == symbolDeclaration && data.contextToken != nil && data.contextToken.Kind == ast.KindExtendsKeyword { // filter out the directly self-recursive type parameters // `type A = K` return false } if isInTypeParameterDefault(data.contextToken) && !ast.IsInferTypeNode(closestSymbolDeclaration.Parent) { typeParameters := closestSymbolDeclaration.Parent.TypeParameterList() if typeParameters != nil && symbolDeclaration.Pos() >= closestSymbolDeclaration.Pos() && symbolDeclaration.Pos() < typeParameters.End() { return false } } } } // External modules can have global export declarations that will be // available as global keywords in all scopes. But if the external module // already has an explicit export and user only wants to use explicit // module imports then the global keywords will be filtered out so auto // import suggestions will win in the completion. symbolOrigin := checker.SkipAlias(symbol, typeChecker) // We only want to filter out the global keywords. // Auto Imports are not available for scripts so this conditional is always false. if file.AsSourceFile().ExternalModuleIndicator != nil && compilerOptions.AllowUmdGlobalAccess != core.TSTrue && data.symbolToSortTextMap[ast.GetSymbolId(symbol)] == SortTextGlobalsOrKeywords && (data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == SortTextAutoImportSuggestions || data.symbolToSortTextMap[ast.GetSymbolId(symbolOrigin)] == SortTextLocationPriority) { return false } allFlags = allFlags | checker.GetCombinedLocalAndExportSymbolFlags(symbolOrigin) // import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace) if isInRightSideOfInternalImportEqualsDeclaration(data.location) { return allFlags&ast.SymbolFlagsNamespace != 0 } if data.isTypeOnlyLocation { // It's a type, but you can reach it by namespace.type as well. return symbolCanBeReferencedAtTypeLocation(symbol, typeChecker, collections.Set[ast.SymbolId]{}) } // expressions are value space (which includes the value namespaces) return allFlags&ast.SymbolFlagsValue != 0 } func getCompletionEntryDisplayNameForSymbol( symbol *ast.Symbol, origin *symbolOriginInfo, completionKind CompletionKind, isJsxIdentifierExpected bool, ) (displayName string, needsConvertPropertyAccess bool) { if originIsIgnore(origin) { return "", false } var name string if originIncludesSymbolName(origin) { name = origin.symbolName() } else { name = ast.SymbolName(symbol) } if name == "" || // If the symbol is external module, don't show it in the completion list // (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there) symbol.Flags&ast.SymbolFlagsModule != 0 && startsWithQuote(name) || // If the symbol is the internal name of an ES symbol, it is not a valid entry. Internal names for ES symbols start with "__@" checker.IsKnownSymbol(symbol) { return "", false } variant := core.IfElse(isJsxIdentifierExpected, core.LanguageVariantJSX, core.LanguageVariantStandard) // name is a valid identifier or private identifier text if scanner.IsIdentifierText(name, variant) || symbol.ValueDeclaration != nil && ast.IsPrivateIdentifierClassElementDeclaration(symbol.ValueDeclaration) { return name, false } if symbol.Flags&ast.SymbolFlagsAlias != 0 { // Allow non-identifier import/export aliases since we can insert them as string literals return name, true } switch completionKind { case CompletionKindMemberLike: if originIsComputedPropertyName(origin) { return origin.symbolName(), false } return "", false case CompletionKindObjectPropertyDeclaration: // TODO: microsoft/TypeScript#18169 escapedName, _ := core.StringifyJson(name, "", "") return escapedName, false case CompletionKindPropertyAccess, CompletionKindGlobal: // For a 'this.' completion it will be in a global context, but may have a non-identifier name. // Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547 ch, _ := utf8.DecodeRuneInString(name) if ch == ' ' { return "", false } return name, true case CompletionKindNone, CompletionKindString: return name, false default: panic(fmt.Sprintf("Unexpected completion kind: %v", completionKind)) } } // !!! refactor symbolOriginInfo so that we can tell the difference between flags and the kind of data it has func originIsIgnore(origin *symbolOriginInfo) bool { return origin != nil && origin.kind&symbolOriginInfoKindIgnore != 0 } func originIncludesSymbolName(origin *symbolOriginInfo) bool { return originIsExport(origin) || originIsComputedPropertyName(origin) } func originIsExport(origin *symbolOriginInfo) bool { return origin != nil && origin.kind&symbolOriginInfoKindExport != 0 } func originIsComputedPropertyName(origin *symbolOriginInfo) bool { return origin != nil && origin.kind&symbolOriginInfoKindComputedPropertyName != 0 } func originIsObjectLiteralMethod(origin *symbolOriginInfo) bool { return origin != nil && origin.kind&symbolOriginInfoKindObjectLiteralMethod != 0 } func originIsThisType(origin *symbolOriginInfo) bool { return origin != nil && origin.kind&symbolOriginInfoKindThisType != 0 } func originIsTypeOnlyAlias(origin *symbolOriginInfo) bool { return origin != nil && origin.kind&symbolOriginInfoKindTypeOnlyAlias != 0 } func originIsSymbolMember(origin *symbolOriginInfo) bool { return origin != nil && origin.kind&symbolOriginInfoKindSymbolMember != 0 } func originIsNullableMember(origin *symbolOriginInfo) bool { return origin != nil && origin.kind&symbolOriginInfoKindNullable != 0 } func originIsPromise(origin *symbolOriginInfo) bool { return origin != nil && origin.kind&symbolOriginInfoKindPromise != 0 } func getSourceFromOrigin(origin *symbolOriginInfo) string { if originIsExport(origin) { return stringutil.StripQuotes(ast.SymbolName(origin.asExport().moduleSymbol)) } if originIsExport(origin) { return origin.asExport().moduleSpecifier } if originIsThisType(origin) { return string(completionSourceThisProperty) } if originIsTypeOnlyAlias(origin) { return string(completionSourceTypeOnlyAlias) } return "" } // In a scenarion such as `const x = 1 * |`, the context and previous tokens are both `*`. // In `const x = 1 * o|`, the context token is *, and the previous token is `o`. // `contextToken` and `previousToken` can both be nil if we are at the beginning of the file. func getRelevantTokens(position int, file *ast.SourceFile) (contextToken *ast.Node, previousToken *ast.Node) { previousToken = astnav.FindPrecedingToken(file, position) if previousToken != nil && position <= previousToken.End() && (ast.IsMemberName(previousToken) || ast.IsKeywordKind(previousToken.Kind)) { contextToken := astnav.FindPrecedingToken(file, previousToken.Pos()) return contextToken, previousToken } return previousToken, previousToken } // "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " " type CompletionsTriggerCharacter = string func isValidTrigger(file *ast.SourceFile, triggerCharacter CompletionsTriggerCharacter, contextToken *ast.Node, position int) bool { switch triggerCharacter { case ".", "@": return true case "\"", "'", "`": // Only automatically bring up completions if this is an opening quote. return contextToken != nil && isStringLiteralOrTemplate(contextToken) && position == astnav.GetStartOfNode(contextToken, file, false /*includeJSDoc*/)+1 case "#": return contextToken != nil && ast.IsPrivateIdentifier(contextToken) && ast.GetContainingClass(contextToken) != nil case "<": // Opening JSX tag return contextToken != nil && contextToken.Kind == ast.KindLessThanToken && (!ast.IsBinaryExpression(contextToken.Parent) || binaryExpressionMayBeOpenTag(contextToken.Parent.AsBinaryExpression())) case "/": if contextToken == nil { return false } if ast.IsStringLiteralLike(contextToken) { return tryGetImportFromModuleSpecifier(contextToken) != nil } return contextToken.Kind == ast.KindLessThanSlashToken && ast.IsJsxClosingElement(contextToken.Parent) case " ": return contextToken != nil && contextToken.Kind == ast.KindImportKeyword && contextToken.Parent.Kind == ast.KindSourceFile default: panic("Unknown trigger character: " + triggerCharacter) } } func isStringLiteralOrTemplate(node *ast.Node) bool { switch node.Kind { case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral, ast.KindTemplateExpression, ast.KindTaggedTemplateExpression: return true } return false } func binaryExpressionMayBeOpenTag(binaryExpression *ast.BinaryExpression) bool { return ast.NodeIsMissing(binaryExpression.Left) } func isCheckedFile(file *ast.SourceFile, compilerOptions *core.CompilerOptions) bool { return !ast.IsSourceFileJS(file) || ast.IsCheckJSEnabledForFile(file, compilerOptions) } func isContextTokenValueLocation(contextToken *ast.Node) bool { return contextToken != nil && ((contextToken.Kind == ast.KindTypeOfKeyword && (contextToken.Parent.Kind == ast.KindTypeQuery || ast.IsTypeOfExpression(contextToken.Parent))) || (contextToken.Kind == ast.KindAssertsKeyword && contextToken.Parent.Kind == ast.KindTypePredicate)) } func isPossiblyTypeArgumentPosition(token *ast.Node, sourceFile *ast.SourceFile, typeChecker *checker.Checker) bool { info := getPossibleTypeArgumentsInfo(token, sourceFile) return info != nil && (ast.IsPartOfTypeNode(info.called) || len(getPossibleGenericSignatures(info.called, info.nTypeArguments, typeChecker)) != 0 || isPossiblyTypeArgumentPosition(info.called, sourceFile, typeChecker)) } func isContextTokenTypeLocation(contextToken *ast.Node) bool { if contextToken != nil { parentKind := contextToken.Parent.Kind switch contextToken.Kind { case ast.KindColonToken: return parentKind == ast.KindPropertyDeclaration || parentKind == ast.KindPropertySignature || parentKind == ast.KindParameter || parentKind == ast.KindVariableDeclaration || ast.IsFunctionLikeKind(parentKind) case ast.KindEqualsToken: return parentKind == ast.KindTypeAliasDeclaration || parentKind == ast.KindTypeParameter case ast.KindAsKeyword: return parentKind == ast.KindAsExpression case ast.KindLessThanToken: return parentKind == ast.KindTypeReference || parentKind == ast.KindTypeAssertionExpression case ast.KindExtendsKeyword: return parentKind == ast.KindTypeParameter case ast.KindSatisfiesKeyword: return parentKind == ast.KindSatisfiesExpression } } return false } // True if symbol is a type or a module containing at least one type. func symbolCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules collections.Set[ast.SymbolId]) bool { // Since an alias can be merged with a local declaration, we need to test both the alias and its target. // This code used to just test the result of `skipAlias`, but that would ignore any locally introduced meanings. return nonAliasCanBeReferencedAtTypeLocation(symbol, typeChecker, seenModules) || nonAliasCanBeReferencedAtTypeLocation( checker.SkipAlias(core.IfElse(symbol.ExportSymbol != nil, symbol.ExportSymbol, symbol), typeChecker), typeChecker, seenModules, ) } func nonAliasCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules collections.Set[ast.SymbolId]) bool { return symbol.Flags&ast.SymbolFlagsType != 0 || typeChecker.IsUnknownSymbol(symbol) || symbol.Flags&ast.SymbolFlagsModule != 0 && seenModules.AddIfAbsent(ast.GetSymbolId(symbol)) && core.Some( typeChecker.GetExportsOfModule(symbol), func(e *ast.Symbol) bool { return symbolCanBeReferencedAtTypeLocation(e, typeChecker, seenModules) }) } // Gets all properties on a type, but if that type is a union of several types, // excludes array-like types or callable/constructable types. func getPropertiesForCompletion(t *checker.Type, typeChecker *checker.Checker) []*ast.Symbol { if t.IsUnion() { return core.CheckEachDefined(typeChecker.GetAllPossiblePropertiesOfTypes(t.Types()), "getAllPossiblePropertiesOfTypes() should all be defined.") } else { return core.CheckEachDefined(typeChecker.GetApparentProperties(t), "getApparentProperties() should all be defined.") } } // Given 'a.b.c', returns 'a'. func getLeftMostName(e *ast.Expression) *ast.IdentifierNode { if ast.IsIdentifier(e) { return e } else if ast.IsPropertyAccessExpression(e) { return getLeftMostName(e.Expression()) } else { return nil } } func getFirstSymbolInChain(symbol *ast.Symbol, enclosingDeclaration *ast.Node, typeChecker *checker.Checker) *ast.Symbol { chain := typeChecker.GetAccessibleSymbolChain( symbol, enclosingDeclaration, ast.SymbolFlagsAll, /*meaning*/ false /*useOnlyExternalAliasing*/) if len(chain) > 0 { return chain[0] } if symbol.Parent != nil { if isModuleSymbol(symbol.Parent) { return symbol } return getFirstSymbolInChain(symbol.Parent, enclosingDeclaration, typeChecker) } return nil } func isModuleSymbol(symbol *ast.Symbol) bool { return core.Some(symbol.Declarations, func(decl *ast.Declaration) bool { return decl.Kind == ast.KindSourceFile }) } func getNullableSymbolOriginInfoKind(kind symbolOriginInfoKind, insertQuestionDot bool) symbolOriginInfoKind { if insertQuestionDot { kind |= symbolOriginInfoKindNullable } return kind } func isStaticProperty(symbol *ast.Symbol) bool { return symbol.ValueDeclaration != nil && symbol.ValueDeclaration.ModifierFlags()&ast.ModifierFlagsStatic != 0 && ast.IsClassLike(symbol.ValueDeclaration.Parent) } func getContextualType(previousToken *ast.Node, position int, file *ast.SourceFile, typeChecker *checker.Checker) *checker.Type { parent := previousToken.Parent switch previousToken.Kind { case ast.KindIdentifier: return getContextualTypeFromParent(previousToken, typeChecker, checker.ContextFlagsNone) case ast.KindEqualsToken: switch parent.Kind { case ast.KindVariableDeclaration: return typeChecker.GetContextualType(parent.Initializer(), checker.ContextFlagsNone) case ast.KindBinaryExpression: return typeChecker.GetTypeAtLocation(parent.AsBinaryExpression().Left) case ast.KindJsxAttribute: return typeChecker.GetContextualTypeForJsxAttribute(parent) default: return nil } case ast.KindNewKeyword: return typeChecker.GetContextualType(parent, checker.ContextFlagsNone) case ast.KindCaseKeyword: caseClause := core.IfElse(ast.IsCaseClause(parent), parent, nil) if caseClause != nil { return getSwitchedType(caseClause, typeChecker) } return nil case ast.KindOpenBraceToken: if ast.IsJsxExpression(parent) && !ast.IsJsxElement(parent.Parent) && !ast.IsJsxFragment(parent.Parent) { return typeChecker.GetContextualTypeForJsxAttribute(parent.Parent) } return nil default: argInfo := getArgumentInfoForCompletions(previousToken, position, file, typeChecker) if argInfo != nil { return typeChecker.GetContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex) } else if isEqualityOperatorKind(previousToken.Kind) && ast.IsBinaryExpression(parent) && isEqualityOperatorKind(parent.AsBinaryExpression().OperatorToken.Kind) { // completion at `x ===/**/` return typeChecker.GetTypeAtLocation(parent.AsBinaryExpression().Left) } else { contextualType := typeChecker.GetContextualType(previousToken, checker.ContextFlagsCompletions) if contextualType != nil { return contextualType } return typeChecker.GetContextualType(previousToken, checker.ContextFlagsNone) } } } func getContextualTypeFromParent(node *ast.Expression, typeChecker *checker.Checker, contextFlags checker.ContextFlags) *checker.Type { parent := ast.WalkUpParenthesizedExpressions(node.Parent) switch parent.Kind { case ast.KindNewExpression: return typeChecker.GetContextualType(parent, contextFlags) case ast.KindBinaryExpression: if isEqualityOperatorKind(parent.AsBinaryExpression().OperatorToken.Kind) { return typeChecker.GetTypeAtLocation( core.IfElse(node == parent.AsBinaryExpression().Right, parent.AsBinaryExpression().Left, parent.AsBinaryExpression().Right)) } return typeChecker.GetContextualType(node, contextFlags) case ast.KindCaseClause: return getSwitchedType(parent, typeChecker) default: return typeChecker.GetContextualType(node, contextFlags) } } func getSwitchedType(caseClause *ast.CaseClauseNode, typeChecker *checker.Checker) *checker.Type { return typeChecker.GetTypeAtLocation(caseClause.Parent.Parent.Expression()) } func isEqualityOperatorKind(kind ast.Kind) bool { switch kind { case ast.KindEqualsEqualsEqualsToken, ast.KindEqualsEqualsToken, ast.KindExclamationEqualsEqualsToken, ast.KindExclamationEqualsToken: return true default: return false } } func isLiteral(t *checker.Type) bool { return t.IsStringLiteral() || t.IsNumberLiteral() || t.IsBigIntLiteral() } func getRecommendedCompletion(previousToken *ast.Node, contextualType *checker.Type, typeChecker *checker.Checker) *ast.Symbol { var types []*checker.Type if contextualType.IsUnion() { types = contextualType.Types() } else { types = []*checker.Type{contextualType} } // For a union, return the first one with a recommended completion. return core.FirstNonNil( types, func(t *checker.Type) *ast.Symbol { symbol := t.Symbol() // Don't make a recommended completion for an abstract class. if symbol != nil && symbol.Flags&(ast.SymbolFlagsEnumMember|ast.SymbolFlagsEnum|ast.SymbolFlagsClass) != 0 && !isAbstractConstructorSymbol(symbol) { return getFirstSymbolInChain(symbol, previousToken, typeChecker) } return nil }, ) } func isAbstractConstructorSymbol(symbol *ast.Symbol) bool { if symbol.Flags&ast.SymbolFlagsClass != 0 { declaration := ast.GetClassLikeDeclarationOfSymbol(symbol) return declaration != nil && ast.HasSyntacticModifier(declaration, ast.ModifierFlagsAbstract) } return false } func startsWithQuote(s string) bool { r, _ := utf8.DecodeRuneInString(s) return r == '"' || r == '\'' } func getClosestSymbolDeclaration(contextToken *ast.Node, location *ast.Node) *ast.Declaration { if contextToken == nil { return nil } closestDeclaration := ast.FindAncestorOrQuit(contextToken, func(node *ast.Node) ast.FindAncestorResult { if ast.IsFunctionBlock(node) || isArrowFunctionBody(node) || ast.IsBindingPattern(node) { return ast.FindAncestorQuit } if (ast.IsParameter(node) || ast.IsTypeParameterDeclaration(node)) && !ast.IsIndexSignatureDeclaration(node.Parent) { return ast.FindAncestorTrue } return ast.FindAncestorFalse }) if closestDeclaration == nil { closestDeclaration = ast.FindAncestorOrQuit(location, func(node *ast.Node) ast.FindAncestorResult { if ast.IsFunctionBlock(node) || isArrowFunctionBody(node) || ast.IsBindingPattern(node) { return ast.FindAncestorQuit } if ast.IsVariableDeclaration(node) { return ast.FindAncestorTrue } return ast.FindAncestorFalse }) } return closestDeclaration } func isArrowFunctionBody(node *ast.Node) bool { return node.Parent != nil && ast.IsArrowFunction(node.Parent) && (node.Parent.Body() == node || // const a = () => /**/; node.Kind == ast.KindEqualsGreaterThanToken) } func isInTypeParameterDefault(contextToken *ast.Node) bool { if contextToken == nil { return false } node := contextToken parent := contextToken.Parent for parent != nil { if ast.IsTypeParameterDeclaration(parent) { return parent.AsTypeParameter().DefaultType == node || node.Kind == ast.KindEqualsToken } node = parent parent = parent.Parent } return false } func isDeprecated(symbol *ast.Symbol, typeChecker *checker.Checker) bool { declarations := checker.SkipAlias(symbol, typeChecker).Declarations return len(declarations) > 0 && core.Every(declarations, func(decl *ast.Declaration) bool { return typeChecker.IsDeprecatedDeclaration(decl) }) } func (l *LanguageService) getReplacementRangeForContextToken(file *ast.SourceFile, contextToken *ast.Node, position int) *lsproto.Range { if contextToken == nil { return nil } // !!! ensure range is single line switch contextToken.Kind { case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral: return l.createRangeFromStringLiteralLikeContent(file, contextToken, position) default: return l.createLspRangeFromNode(contextToken, file) } } func (l *LanguageService) createRangeFromStringLiteralLikeContent(file *ast.SourceFile, node *ast.StringLiteralLike, position int) *lsproto.Range { replacementEnd := node.End() - 1 nodeStart := astnav.GetStartOfNode(node, file, false /*includeJSDoc*/) if ast.IsUnterminatedLiteral(node) { // we return no replacement range only if unterminated string is empty if nodeStart == replacementEnd { return nil } replacementEnd = min(position, node.End()) } return l.createLspRangeFromBounds(nodeStart+1, replacementEnd, file) } func quotePropertyName(file *ast.SourceFile, preferences *UserPreferences, name string) string { r, _ := utf8.DecodeRuneInString(name) if unicode.IsDigit(r) { return name } return quote(file, preferences, name) } // Checks whether type is `string & {}`, which is semantically equivalent to string but // is not reduced by the checker as a special case used for supporting string literal completions // for string type. func isStringAndEmptyAnonymousObjectIntersection(typeChecker *checker.Checker, t *checker.Type) bool { if !t.IsIntersection() { return false } return len(t.Types()) == 2 && (areIntersectedTypesAvoidingStringReduction(typeChecker, t.Types()[0], t.Types()[1]) || areIntersectedTypesAvoidingStringReduction(typeChecker, t.Types()[1], t.Types()[0])) } func areIntersectedTypesAvoidingStringReduction(typeChecker *checker.Checker, t1 *checker.Type, t2 *checker.Type) bool { return t1.IsString() && typeChecker.IsEmptyAnonymousObjectType(t2) } func escapeSnippetText(text string) string { return strings.ReplaceAll(text, `$`, `\$`) } func isNamedImportsOrExports(node *ast.Node) bool { return ast.IsNamedImports(node) || ast.IsNamedExports(node) } func generateIdentifierForArbitraryString(text string) string { needsUnderscore := false identifier := "" var ch rune var size int // Convert "(example, text)" into "_example_text_" for pos := 0; pos < len(text); pos += size { ch, size = utf8.DecodeRuneInString(text[pos:]) var validChar bool if pos == 0 { validChar = scanner.IsIdentifierStart(ch) } else { validChar = scanner.IsIdentifierPart(ch) } if size > 0 && validChar { if needsUnderscore { identifier += "_" } identifier += string(ch) needsUnderscore = false } else { needsUnderscore = true } } if needsUnderscore { identifier += "_" } // Default to "_" if the provided text was empty if identifier == "" { return "_" } return identifier } // Copied from vscode TS extension. func getCompletionsSymbolKind(kind ScriptElementKind) lsproto.CompletionItemKind { switch kind { case ScriptElementKindPrimitiveType, ScriptElementKindKeyword: return lsproto.CompletionItemKindKeyword case ScriptElementKindConstElement, ScriptElementKindLetElement, ScriptElementKindVariableElement, ScriptElementKindLocalVariableElement, ScriptElementKindAlias, ScriptElementKindParameterElement: return lsproto.CompletionItemKindVariable case ScriptElementKindMemberVariableElement, ScriptElementKindMemberGetAccessorElement, ScriptElementKindMemberSetAccessorElement: return lsproto.CompletionItemKindField case ScriptElementKindFunctionElement, ScriptElementKindLocalFunctionElement: return lsproto.CompletionItemKindFunction case ScriptElementKindMemberFunctionElement, ScriptElementKindConstructSignatureElement, ScriptElementKindCallSignatureElement, ScriptElementKindIndexSignatureElement: return lsproto.CompletionItemKindMethod case ScriptElementKindEnumElement: return lsproto.CompletionItemKindEnum case ScriptElementKindEnumMemberElement: return lsproto.CompletionItemKindEnumMember case ScriptElementKindModuleElement, ScriptElementKindExternalModuleName: return lsproto.CompletionItemKindModule case ScriptElementKindClassElement, ScriptElementKindTypeElement: return lsproto.CompletionItemKindClass case ScriptElementKindInterfaceElement: return lsproto.CompletionItemKindInterface case ScriptElementKindWarning: return lsproto.CompletionItemKindText case ScriptElementKindScriptElement: return lsproto.CompletionItemKindFile case ScriptElementKindDirectory: return lsproto.CompletionItemKindFolder case ScriptElementKindString: return lsproto.CompletionItemKindConstant default: return lsproto.CompletionItemKindProperty } } // Editors will use the `sortText` and then fall back to `name` for sorting, but leave ties in response order. // So, it's important that we sort those ties in the order we want them displayed if it matters. We don't // strictly need to sort by name or SortText here since clients are going to do it anyway, but we have to // do the work of comparing them so we can sort those ties appropriately; plus, it makes the order returned // by the language service consistent with what TS Server does and what editors typically do. This also makes // completions tests make more sense. We used to sort only alphabetically and only in the server layer, but // this made tests really weird, since most fourslash tests don't use the server. func compareCompletionEntries(entryInSlice *lsproto.CompletionItem, entryToInsert *lsproto.CompletionItem) int { compareStrings := stringutil.CompareStringsCaseInsensitiveThenSensitive result := compareStrings(*entryInSlice.SortText, *entryToInsert.SortText) if result == stringutil.ComparisonEqual { result = compareStrings(entryInSlice.Label, entryToInsert.Label) } if result == stringutil.ComparisonEqual && entryInSlice.Data != nil && entryToInsert.Data != nil { sliceEntryData, ok1 := (*entryInSlice.Data).(*completionEntryData) insertEntryData, ok2 := (*entryToInsert.Data).(*completionEntryData) if ok1 && ok2 && sliceEntryData.ModuleSpecifier != "" && insertEntryData.ModuleSpecifier != "" { // Sort same-named auto-imports by module specifier result = compareNumberOfDirectorySeparators( sliceEntryData.ModuleSpecifier, insertEntryData.ModuleSpecifier, ) } } if result == stringutil.ComparisonEqual { // Fall back to symbol order - if we return `EqualTo`, `insertSorted` will put later symbols first. return stringutil.ComparisonLessThan } return result } // True if the first character of `lowercaseCharacters` is the first character // of some "word" in `identiferString` (where the string is split into "words" // by camelCase and snake_case segments), then if the remaining characters of // `lowercaseCharacters` appear, in order, in the rest of `identifierString`.// // True: // 'state' in 'useState' // 'sae' in 'useState' // 'viable' in 'ENVIRONMENT_VARIABLE'// // False: // 'staet' in 'useState' // 'tate' in 'useState' // 'ment' in 'ENVIRONMENT_VARIABLE' func charactersFuzzyMatchInString(identifierString string, lowercaseCharacters string) bool { if lowercaseCharacters == "" { return true } var prevChar rune matchedFirstCharacter := false characterIndex := 0 lowerCaseRunes := []rune(lowercaseCharacters) testChar := lowerCaseRunes[characterIndex] for _, strChar := range []rune(identifierString) { if strChar == testChar || strChar == unicode.ToUpper(testChar) { willMatchFirstChar := prevChar == 0 || // Beginning of word 'a' <= prevChar && prevChar <= 'z' && 'A' <= strChar && strChar <= 'Z' || // camelCase transition prevChar == '_' && strChar != '_' // snake_case transition matchedFirstCharacter = matchedFirstCharacter || willMatchFirstChar if !matchedFirstCharacter { continue } characterIndex++ if characterIndex == len(lowerCaseRunes) { return true } else { testChar = lowerCaseRunes[characterIndex] } } prevChar = strChar } // Did not find all characters return false } var ( keywordCompletionsCache = collections.SyncMap[KeywordCompletionFilters, []*lsproto.CompletionItem]{} allKeywordCompletions = sync.OnceValue(func() []*lsproto.CompletionItem { result := make([]*lsproto.CompletionItem, 0, ast.KindLastKeyword-ast.KindFirstKeyword+1) for i := ast.KindFirstKeyword; i <= ast.KindLastKeyword; i++ { result = append(result, &lsproto.CompletionItem{ Label: scanner.TokenToString(i), Kind: ptrTo(lsproto.CompletionItemKindKeyword), SortText: ptrTo(string(SortTextGlobalsOrKeywords)), }) } return result }) ) func cloneItems(items []*lsproto.CompletionItem) []*lsproto.CompletionItem { result := make([]*lsproto.CompletionItem, len(items)) for i, item := range items { itemClone := *item result[i] = &itemClone } return result } func getKeywordCompletions(keywordFilter KeywordCompletionFilters, filterOutTsOnlyKeywords bool) []*lsproto.CompletionItem { if !filterOutTsOnlyKeywords { return cloneItems(getTypescriptKeywordCompletions(keywordFilter)) } index := keywordFilter + KeywordCompletionFiltersLast + 1 if cached, ok := keywordCompletionsCache.Load(index); ok { return cloneItems(cached) } result := core.Filter( getTypescriptKeywordCompletions(keywordFilter), func(ci *lsproto.CompletionItem) bool { return !isTypeScriptOnlyKeyword(scanner.StringToToken(ci.Label)) }) keywordCompletionsCache.Store(index, result) return cloneItems(result) } func getTypescriptKeywordCompletions(keywordFilter KeywordCompletionFilters) []*lsproto.CompletionItem { if cached, ok := keywordCompletionsCache.Load(keywordFilter); ok { return cached } result := core.Filter(allKeywordCompletions(), func(entry *lsproto.CompletionItem) bool { kind := scanner.StringToToken(entry.Label) switch keywordFilter { case KeywordCompletionFiltersNone: return false case KeywordCompletionFiltersAll: return isFunctionLikeBodyKeyword(kind) || kind == ast.KindDeclareKeyword || kind == ast.KindModuleKeyword || kind == ast.KindTypeKeyword || kind == ast.KindNamespaceKeyword || kind == ast.KindAbstractKeyword || isTypeKeyword(kind) && kind != ast.KindUndefinedKeyword case KeywordCompletionFiltersFunctionLikeBodyKeywords: return isFunctionLikeBodyKeyword(kind) case KeywordCompletionFiltersClassElementKeywords: return isClassMemberCompletionKeyword(kind) case KeywordCompletionFiltersInterfaceElementKeywords: return isInterfaceOrTypeLiteralCompletionKeyword(kind) case KeywordCompletionFiltersConstructorParameterKeywords: return ast.IsParameterPropertyModifier(kind) case KeywordCompletionFiltersTypeAssertionKeywords: return isTypeKeyword(kind) || kind == ast.KindConstKeyword case KeywordCompletionFiltersTypeKeywords: return isTypeKeyword(kind) case KeywordCompletionFiltersTypeKeyword: return kind == ast.KindTypeKeyword default: panic(fmt.Sprintf("Unknown keyword filter: %v", keywordFilter)) } }) keywordCompletionsCache.Store(keywordFilter, result) return result } func isTypeScriptOnlyKeyword(kind ast.Kind) bool { switch kind { case ast.KindAbstractKeyword, ast.KindAnyKeyword, ast.KindBigIntKeyword, ast.KindBooleanKeyword, ast.KindDeclareKeyword, ast.KindEnumKeyword, ast.KindGlobalKeyword, ast.KindImplementsKeyword, ast.KindInferKeyword, ast.KindInterfaceKeyword, ast.KindIsKeyword, ast.KindKeyOfKeyword, ast.KindModuleKeyword, ast.KindNamespaceKeyword, ast.KindNeverKeyword, ast.KindNumberKeyword, ast.KindObjectKeyword, ast.KindOverrideKeyword, ast.KindPrivateKeyword, ast.KindProtectedKeyword, ast.KindPublicKeyword, ast.KindReadonlyKeyword, ast.KindStringKeyword, ast.KindSymbolKeyword, ast.KindTypeKeyword, ast.KindUniqueKeyword, ast.KindUnknownKeyword: return true default: return false } } func isFunctionLikeBodyKeyword(kind ast.Kind) bool { return kind == ast.KindAsyncKeyword || kind == ast.KindAwaitKeyword || kind == ast.KindUsingKeyword || kind == ast.KindAsKeyword || kind == ast.KindSatisfiesKeyword || kind == ast.KindTypeKeyword || !ast.IsContextualKeyword(kind) && !isClassMemberCompletionKeyword(kind) } func isClassMemberCompletionKeyword(kind ast.Kind) bool { switch kind { case ast.KindAbstractKeyword, ast.KindAccessorKeyword, ast.KindConstructorKeyword, ast.KindGetKeyword, ast.KindSetKeyword, ast.KindAsyncKeyword, ast.KindDeclareKeyword, ast.KindOverrideKeyword: return true default: return ast.IsClassMemberModifier(kind) } } func isInterfaceOrTypeLiteralCompletionKeyword(kind ast.Kind) bool { return kind == ast.KindReadonlyKeyword } func isContextualKeywordInAutoImportableExpressionSpace(keyword string) bool { return keyword == "abstract" || keyword == "async" || keyword == "await" || keyword == "declare" || keyword == "module" || keyword == "namespace" || keyword == "type" || keyword == "satisfies" || keyword == "as" } func getContextualKeywords(file *ast.SourceFile, contextToken *ast.Node, position int) []*lsproto.CompletionItem { var entries []*lsproto.CompletionItem // An `AssertClause` can come after an import declaration: // import * from "foo" | // import "foo" | // or after a re-export declaration that has a module specifier: // export { foo } from "foo" | // Source: https://tc39.es/proposal-import-assertions/ if contextToken != nil { parent := contextToken.Parent tokenLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, contextToken.End()) currentLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, position) if (ast.IsImportDeclaration(parent) || ast.IsExportDeclaration(parent) && parent.AsExportDeclaration().ModuleSpecifier != nil) && contextToken == parent.ModuleSpecifier() && tokenLine == currentLine { entries = append(entries, &lsproto.CompletionItem{ Label: scanner.TokenToString(ast.KindAssertKeyword), Kind: ptrTo(lsproto.CompletionItemKindKeyword), SortText: ptrTo(string(SortTextGlobalsOrKeywords)), }) } } return entries } func (l *LanguageService) getJSCompletionEntries( ctx context.Context, file *ast.SourceFile, position int, uniqueNames *collections.Set[string], sortedEntries []*lsproto.CompletionItem, ) []*lsproto.CompletionItem { nameTable := getNameTable(file) for name, pos := range nameTable { // Skip identifiers produced only from the current location if pos == position { continue } if !uniqueNames.Has(name) && scanner.IsIdentifierText(name, core.LanguageVariantStandard) { uniqueNames.Add(name) sortedEntries = core.InsertSorted( sortedEntries, &lsproto.CompletionItem{ Label: name, Kind: ptrTo(lsproto.CompletionItemKindText), SortText: ptrTo(string(SortTextJavascriptIdentifiers)), CommitCharacters: ptrTo([]string{}), }, compareCompletionEntries, ) } } return sortedEntries } func (l *LanguageService) getOptionalReplacementSpan(location *ast.Node, file *ast.SourceFile) *lsproto.Range { // StringLiteralLike locations are handled separately in stringCompletions.ts if location != nil && (location.Kind == ast.KindIdentifier || location.Kind == ast.KindPrivateIdentifier) { start := astnav.GetStartOfNode(location, file, false /*includeJSDoc*/) return l.createLspRangeFromBounds(start, location.End(), file) } return nil } func isMemberCompletionKind(kind CompletionKind) bool { return kind == CompletionKindObjectPropertyDeclaration || kind == CompletionKindMemberLike || kind == CompletionKindPropertyAccess } func tryGetFunctionLikeBodyCompletionContainer(contextToken *ast.Node) *ast.Node { if contextToken == nil { return nil } var prev *ast.Node container := ast.FindAncestorOrQuit(contextToken, func(node *ast.Node) ast.FindAncestorResult { if ast.IsClassLike(node) { return ast.FindAncestorQuit } if ast.IsFunctionLikeDeclaration(node) && prev == node.Body() { return ast.FindAncestorTrue } prev = node return ast.FindAncestorFalse }) return container } func computeCommitCharactersAndIsNewIdentifier( contextToken *ast.Node, file *ast.SourceFile, position int, ) (isNewIdentifierLocation bool, defaultCommitCharacters []string) { if contextToken == nil { return false, allCommitCharacters } containingNodeKind := contextToken.Parent.Kind tokenKind := keywordForNode(contextToken) // Previous token may have been a keyword that was converted to an identifier. switch tokenKind { case ast.KindCommaToken: switch containingNodeKind { // func( a, | // new C(a, | case ast.KindCallExpression, ast.KindNewExpression: expression := contextToken.Parent.Expression() // func\n(a, | if getLineOfPosition(file, expression.End()) != getLineOfPosition(file, position) { return true, noCommaCommitCharacters } return true, allCommitCharacters // const x = (a, | case ast.KindBinaryExpression: return true, noCommaCommitCharacters // constructor( a, | /* public, protected, private keywords are allowed here, so show completion */ // var x: (s: string, list| // const obj = { x, | case ast.KindConstructor, ast.KindFunctionType, ast.KindObjectLiteralExpression: return true, emptyCommitCharacters // [a, | case ast.KindArrayLiteralExpression: return true, allCommitCharacters default: return false, allCommitCharacters } case ast.KindOpenParenToken: switch containingNodeKind { // func( | // new C(a| case ast.KindCallExpression, ast.KindNewExpression: expression := contextToken.Parent.Expression() // func\n( | if getLineOfPosition(file, expression.End()) != getLineOfPosition(file, position) { return true, noCommaCommitCharacters } return true, allCommitCharacters // const x = (a| case ast.KindParenthesizedExpression: return true, noCommaCommitCharacters // constructor( | // function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */ case ast.KindConstructor, ast.KindParenthesizedType: return true, emptyCommitCharacters default: return false, allCommitCharacters } case ast.KindOpenBracketToken: switch containingNodeKind { // [ | // [ | : string ] // [ | : string ] // [ | /* this can become an index signature */ case ast.KindArrayLiteralExpression, ast.KindIndexSignature, ast.KindTupleType, ast.KindComputedPropertyName: return true, allCommitCharacters default: return false, allCommitCharacters } // module | // namespace | // import | case ast.KindModuleKeyword, ast.KindNamespaceKeyword, ast.KindImportKeyword: return true, emptyCommitCharacters case ast.KindDotToken: switch containingNodeKind { // module A.| case ast.KindModuleDeclaration: return true, emptyCommitCharacters default: return false, allCommitCharacters } case ast.KindOpenBraceToken: switch containingNodeKind { // class A { | // const obj = { | case ast.KindClassDeclaration, ast.KindObjectLiteralExpression: return true, emptyCommitCharacters default: return false, allCommitCharacters } case ast.KindEqualsToken: switch containingNodeKind { // const x = a| // x = a| case ast.KindVariableDeclaration, ast.KindBinaryExpression: return true, allCommitCharacters default: return false, allCommitCharacters } case ast.KindTemplateHead: // `aa ${| return containingNodeKind == ast.KindTemplateExpression, allCommitCharacters case ast.KindTemplateMiddle: // `aa ${10} dd ${| return containingNodeKind == ast.KindTemplateSpan, allCommitCharacters case ast.KindAsyncKeyword: // const obj = { async c|() // const obj = { async c| if containingNodeKind == ast.KindMethodDeclaration || containingNodeKind == ast.KindShorthandPropertyAssignment { return true, emptyCommitCharacters } return false, allCommitCharacters case ast.KindAsteriskToken: // const obj = { * c| if containingNodeKind == ast.KindMethodDeclaration { return true, emptyCommitCharacters } return false, allCommitCharacters } if isClassMemberCompletionKeyword(tokenKind) { return true, emptyCommitCharacters } return false, allCommitCharacters } func keywordForNode(node *ast.Node) ast.Kind { if ast.IsIdentifier(node) { return scanner.IdentifierToKeywordKind(node.AsIdentifier()) } return node.Kind } // Finds the first node that "embraces" the position, so that one may // accurately aggregate locals from the closest containing scope. func getScopeNode(initialToken *ast.Node, position int, file *ast.SourceFile) *ast.Node { scope := initialToken for scope != nil && !positionBelongsToNode(scope, position, file) { scope = scope.Parent } return scope } func isSnippetScope(scopeNode *ast.Node) bool { switch scopeNode.Kind { case ast.KindSourceFile, ast.KindTemplateExpression, ast.KindJsxExpression, ast.KindBlock: return true default: return ast.IsStatement(scopeNode) } } // Determines if a type is exactly the same type resolved by the global 'self', 'global', or 'globalThis'. func isProbablyGlobalType(t *checker.Type, file *ast.SourceFile, typeChecker *checker.Checker) bool { // The type of `self` and `window` is the same in lib.dom.d.ts, but `window` does not exist in // lib.webworker.d.ts, so checking against `self` is also a check against `window` when it exists. selfSymbol := typeChecker.GetGlobalSymbol("self", ast.SymbolFlagsValue, nil /*diagnostic*/) if selfSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(selfSymbol, file.AsNode()) == t { return true } globalSymbol := typeChecker.GetGlobalSymbol("global", ast.SymbolFlagsValue, nil /*diagnostic*/) if globalSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(globalSymbol, file.AsNode()) == t { return true } globalThisSymbol := typeChecker.GetGlobalSymbol("globalThis", ast.SymbolFlagsValue, nil /*diagnostic*/) if globalThisSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(globalThisSymbol, file.AsNode()) == t { return true } return false } func tryGetTypeLiteralNode(node *ast.Node) *ast.TypeLiteral { if node == nil { return nil } parent := node.Parent switch node.Kind { case ast.KindOpenBraceToken: if ast.IsTypeLiteralNode(parent) { return parent } case ast.KindSemicolonToken, ast.KindCommaToken, ast.KindIdentifier: if parent.Kind == ast.KindPropertySignature && ast.IsTypeLiteralNode(parent.Parent) { return parent.Parent } } return nil } func getConstraintOfTypeArgumentProperty(node *ast.Node, typeChecker *checker.Checker) *checker.Type { if node == nil { return nil } if ast.IsTypeNode(node) && ast.IsTypeReferenceType(node.Parent) { return typeChecker.GetTypeArgumentConstraint(node) } t := getConstraintOfTypeArgumentProperty(node.Parent, typeChecker) if t == nil { return nil } switch node.Kind { case ast.KindPropertySignature: return typeChecker.GetTypeOfPropertyOfContextualType(t, node.Symbol().Name) case ast.KindIntersectionType, ast.KindTypeLiteral, ast.KindUnionType: return t } return nil } func tryGetObjectLikeCompletionContainer(contextToken *ast.Node, position int, file *ast.SourceFile) *ast.ObjectLiteralLike { if contextToken == nil { return nil } parent := contextToken.Parent switch contextToken.Kind { // const x = { | // const x = { a: 0, | case ast.KindOpenBraceToken, ast.KindCommaToken: if ast.IsObjectLiteralExpression(parent) || ast.IsObjectBindingPattern(parent) { return parent } case ast.KindAsteriskToken: if ast.IsMethodDeclaration(parent) && ast.IsObjectLiteralExpression(parent.Parent) { return parent.Parent } case ast.KindAsyncKeyword: if ast.IsObjectLiteralExpression(parent.Parent) { return parent.Parent } case ast.KindIdentifier: if contextToken.Text() == "async" && ast.IsShorthandPropertyAssignment(parent) { return parent.Parent } else { if ast.IsObjectLiteralExpression(parent.Parent) && (ast.IsSpreadAssignment(parent) || ast.IsShorthandPropertyAssignment(parent) && getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position)) { return parent.Parent } ancestorNode := ast.FindAncestor(parent, ast.IsPropertyAssignment) if ancestorNode != nil && lsutil.GetLastToken(ancestorNode, file) == contextToken && ast.IsObjectLiteralExpression(ancestorNode.Parent) { return ancestorNode.Parent } } default: if parent.Parent != nil && parent.Parent.Parent != nil && (ast.IsMethodDeclaration(parent.Parent) || ast.IsGetAccessorDeclaration(parent.Parent) || ast.IsSetAccessorDeclaration(parent.Parent)) && ast.IsObjectLiteralExpression(parent.Parent.Parent) { return parent.Parent.Parent } if ast.IsSpreadAssignment(parent) && ast.IsObjectLiteralExpression(parent.Parent) { return parent.Parent } ancestorNode := ast.FindAncestor(parent, ast.IsPropertyAssignment) if contextToken.Kind != ast.KindColonToken && ancestorNode != nil && lsutil.GetLastToken(ancestorNode, file) == contextToken && ast.IsObjectLiteralExpression(ancestorNode.Parent) { return ancestorNode.Parent } } return nil } func tryGetObjectLiteralContextualType(node *ast.ObjectLiteralExpressionNode, typeChecker *checker.Checker) *checker.Type { t := typeChecker.GetContextualType(node, checker.ContextFlagsNone) if t != nil { return t } parent := ast.WalkUpParenthesizedExpressions(node.Parent) if ast.IsBinaryExpression(parent) && parent.AsBinaryExpression().OperatorToken.Kind == ast.KindEqualsToken && node == parent.AsBinaryExpression().Left { // Object literal is assignment pattern: ({ | } = x) return typeChecker.GetTypeAtLocation(parent) } if ast.IsExpression(parent) { // f(() => (({ | }))); return typeChecker.GetContextualType(parent, checker.ContextFlagsNone) } return nil } func getPropertiesForObjectExpression( contextualType *checker.Type, completionsType *checker.Type, obj *ast.Node, typeChecker *checker.Checker, ) []*ast.Symbol { hasCompletionsType := completionsType != nil && completionsType != contextualType var types []*checker.Type if contextualType.IsUnion() { types = contextualType.Types() } else { types = []*checker.Type{contextualType} } promiseFilteredContextualType := typeChecker.GetUnionType(core.Filter(types, func(t *checker.Type) bool { return typeChecker.GetPromisedTypeOfPromise(t) == nil })) var t *checker.Type if hasCompletionsType && completionsType.Flags()&checker.TypeFlagsAnyOrUnknown == 0 { t = typeChecker.GetUnionType([]*checker.Type{promiseFilteredContextualType, completionsType}) } else { t = promiseFilteredContextualType } // Filter out members whose only declaration is the object literal itself to avoid // self-fulfilling completions like: // // function f(x: T) {} // f({ abc/**/: "" }) // `abc` is a member of `T` but only because it declares itself hasDeclarationOtherThanSelf := func(member *ast.Symbol) bool { if len(member.Declarations) == 0 { return true } return core.Some(member.Declarations, func(decl *ast.Declaration) bool { return decl.Parent != obj }) } properties := getApparentProperties(t, obj, typeChecker) if t.IsClass() && containsNonPublicProperties(properties) { return nil } else if hasCompletionsType { return core.Filter(properties, hasDeclarationOtherThanSelf) } else { return properties } } func getApparentProperties(t *checker.Type, node *ast.Node, typeChecker *checker.Checker) []*ast.Symbol { if !t.IsUnion() { return typeChecker.GetApparentProperties(t) } return typeChecker.GetAllPossiblePropertiesOfTypes(core.Filter(t.Types(), func(memberType *checker.Type) bool { return !(memberType.Flags()&checker.TypeFlagsPrimitive != 0 || typeChecker.IsArrayLikeType(memberType) || typeChecker.IsTypeInvalidDueToUnionDiscriminant(memberType, node) || typeChecker.TypeHasCallOrConstructSignatures(memberType) || memberType.IsClass() && containsNonPublicProperties(typeChecker.GetApparentProperties(memberType))) })) } func containsNonPublicProperties(props []*ast.Symbol) bool { return core.Some(props, func(p *ast.Symbol) bool { return checker.GetDeclarationModifierFlagsFromSymbol(p)&ast.ModifierFlagsNonPublicAccessibilityModifier != 0 }) } // Filters out members that are already declared in the object literal or binding pattern. // Also computes the set of existing members declared by spread assignment. func filterObjectMembersList( contextualMemberSymbols []*ast.Symbol, existingMembers []*ast.Declaration, file *ast.SourceFile, position int, typeChecker *checker.Checker, ) (filteredMembers []*ast.Symbol, spreadMemberNames collections.Set[string]) { if len(existingMembers) == 0 { return contextualMemberSymbols, collections.Set[string]{} } membersDeclaredBySpreadAssignment := collections.Set[string]{} existingMemberNames := collections.Set[string]{} for _, member := range existingMembers { // Ignore omitted expressions for missing members. if member.Kind != ast.KindPropertyAssignment && member.Kind != ast.KindShorthandPropertyAssignment && member.Kind != ast.KindBindingElement && member.Kind != ast.KindMethodDeclaration && member.Kind != ast.KindGetAccessor && member.Kind != ast.KindSetAccessor && member.Kind != ast.KindSpreadAssignment { continue } // If this is the current item we are editing right now, do not filter it out. if isCurrentlyEditingNode(member, file, position) { continue } var existingName string if ast.IsSpreadAssignment(member) { setMemberDeclaredBySpreadAssignment(member, &membersDeclaredBySpreadAssignment, typeChecker) } else if ast.IsBindingElement(member) && member.AsBindingElement().PropertyName != nil { // include only identifiers in completion list if member.AsBindingElement().PropertyName.Kind == ast.KindIdentifier { existingName = member.AsBindingElement().PropertyName.Text() } } else { // TODO: Account for computed property name // NOTE: if one only performs this step when m.name is an identifier, // things like '__proto__' are not filtered out. name := ast.GetNameOfDeclaration(member) if name != nil && ast.IsPropertyNameLiteral(name) { existingName = name.Text() } } if existingName != "" { existingMemberNames.Add(existingName) } } filteredSymbols := core.Filter(contextualMemberSymbols, func(m *ast.Symbol) bool { return !existingMemberNames.Has(m.Name) }) return filteredSymbols, membersDeclaredBySpreadAssignment } func isCurrentlyEditingNode(node *ast.Node, file *ast.SourceFile, position int) bool { start := astnav.GetStartOfNode(node, file, false /*includeJSDoc*/) return start <= position && position <= node.End() } func setMemberDeclaredBySpreadAssignment(declaration *ast.Node, members *collections.Set[string], typeChecker *checker.Checker) { expression := declaration.Expression() symbol := typeChecker.GetSymbolAtLocation(expression) var t *checker.Type if symbol != nil { t = typeChecker.GetTypeOfSymbolAtLocation(symbol, expression) } var properties []*ast.Symbol if t != nil { properties = t.AsStructuredType().Properties() } for _, property := range properties { members.Add(property.Name) } } // Returns the immediate owning class declaration of a context token, // on the condition that one exists and that the context implies completion should be given. func tryGetConstructorLikeCompletionContainer(contextToken *ast.Node) *ast.ConstructorDeclarationNode { if contextToken == nil { return nil } parent := contextToken.Parent switch contextToken.Kind { case ast.KindOpenParenToken, ast.KindCommaToken: if ast.IsConstructorDeclaration(parent) { return parent } return nil default: if isConstructorParameterCompletion(contextToken) { return parent.Parent } } return nil } func isConstructorParameterCompletion(node *ast.Node) bool { return node.Parent != nil && ast.IsParameter(node.Parent) && ast.IsConstructorDeclaration(node.Parent.Parent) && (ast.IsParameterPropertyModifier(node.Kind) || ast.IsDeclarationName(node)) } // Returns the immediate owning class declaration of a context token, // on the condition that one exists and that the context implies completion should be given. func tryGetObjectTypeDeclarationCompletionContainer( file *ast.SourceFile, contextToken *ast.Node, location *ast.Node, position int, ) *ast.ObjectTypeDeclaration { // class c { method() { } | method2() { } } switch location.Kind { case ast.KindSyntaxList: if ast.IsObjectTypeDeclaration(location.Parent) { return location.Parent } return nil case ast.KindEndOfFile: stmtList := location.Parent.AsSourceFile().Statements if stmtList != nil && len(stmtList.Nodes) > 0 && ast.IsObjectTypeDeclaration(stmtList.Nodes[len(stmtList.Nodes)-1]) { cls := stmtList.Nodes[len(stmtList.Nodes)-1] if findChildOfKind(cls, ast.KindCloseBraceToken, file) == nil { return cls } } case ast.KindPrivateIdentifier: if ast.IsPropertyDeclaration(location.Parent) { return ast.FindAncestor(location, ast.IsClassLike) } case ast.KindIdentifier: originalKeywordKind := scanner.IdentifierToKeywordKind(location.AsIdentifier()) if originalKeywordKind != ast.KindUnknown { return nil } // class c { public prop = c| } if ast.IsPropertyDeclaration(location.Parent) && location.Parent.Initializer() == location { return nil } // class c extends React.Component { a: () => 1\n compon| } if isFromObjectTypeDeclaration(location) { return ast.FindAncestor(location, ast.IsObjectTypeDeclaration) } } if contextToken == nil { return nil } // class C { blah; constructor/**/ } // or // class C { blah \n constructor/**/ } if location.Kind == ast.KindConstructorKeyword || (ast.IsIdentifier(contextToken) && ast.IsPropertyDeclaration(contextToken.Parent) && ast.IsClassLike(location)) { return ast.FindAncestor(contextToken, ast.IsClassLike) } switch contextToken.Kind { // class c { public prop = | /* global completions */ } case ast.KindEqualsToken: return nil // class c {getValue(): number; | } // class c { method() { } | } case ast.KindSemicolonToken, ast.KindCloseBraceToken: // class c { method() { } b| } if isFromObjectTypeDeclaration(location) && location.Parent.Name() == location { return location.Parent.Parent } if ast.IsObjectTypeDeclaration(location) { return location } return nil // class c { | // class c {getValue(): number, | } case ast.KindOpenBraceToken, ast.KindCommaToken: if ast.IsObjectTypeDeclaration(contextToken.Parent) { return contextToken.Parent } return nil default: if ast.IsObjectTypeDeclaration(location) { // class C extends React.Component { a: () => 1\n| } // class C { prop = ""\n | } if getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position) { return location } isValidKeyword := core.IfElse( ast.IsClassLike(contextToken.Parent.Parent), isClassMemberCompletionKeyword, isInterfaceOrTypeLiteralCompletionKeyword, ) if isValidKeyword(contextToken.Kind) || contextToken.Kind == ast.KindAsteriskToken || ast.IsIdentifier(contextToken) && isValidKeyword(scanner.IdentifierToKeywordKind(contextToken.AsIdentifier())) { return contextToken.Parent.Parent } } return nil } } func isFromObjectTypeDeclaration(node *ast.Node) bool { return node.Parent != nil && ast.IsClassOrTypeElement(node.Parent) && ast.IsObjectTypeDeclaration(node.Parent.Parent) } // Filters out completion suggestions for class elements. func filterClassMembersList( baseSymbols []*ast.Symbol, existingMembers []*ast.ClassElement, classElementModifierFlags ast.ModifierFlags, file *ast.SourceFile, position int, ) []*ast.Symbol { existingMemberNames := collections.Set[string]{} for _, member := range existingMembers { // Ignore omitted expressions for missing members. if member.Kind != ast.KindPropertyDeclaration && member.Kind != ast.KindMethodDeclaration && member.Kind != ast.KindGetAccessor && member.Kind != ast.KindSetAccessor { continue } // If this is the current item we are editing right now, do not filter it out if isCurrentlyEditingNode(member, file, position) { continue } // Don't filter member even if the name matches if it is declared private in the list. if member.ModifierFlags()&ast.ModifierFlagsPrivate != 0 { continue } // Do not filter it out if the static presence doesn't match. if ast.IsStatic(member) != (classElementModifierFlags&ast.ModifierFlagsStatic != 0) { continue } existingName := ast.GetPropertyNameForPropertyNameNode(member.Name()) if existingName != "" { existingMemberNames.Add(existingName) } } return core.Filter(baseSymbols, func(propertySymbol *ast.Symbol) bool { return !existingMemberNames.Has(ast.SymbolName(propertySymbol)) && len(propertySymbol.Declarations) > 0 && checker.GetDeclarationModifierFlagsFromSymbol(propertySymbol)&ast.ModifierFlagsPrivate == 0 && !(propertySymbol.ValueDeclaration != nil && ast.IsPrivateIdentifierClassElementDeclaration(propertySymbol.ValueDeclaration)) }) } func tryGetContainingJsxElement(contextToken *ast.Node, file *ast.SourceFile) *ast.JsxOpeningLikeElement { if contextToken == nil { return nil } parent := contextToken.Parent switch contextToken.Kind { case ast.KindGreaterThanToken, ast.KindLessThanSlashToken, ast.KindSlashToken, ast.KindIdentifier, ast.KindPropertyAccessExpression, ast.KindJsxAttributes, ast.KindJsxAttribute, ast.KindJsxSpreadAttribute: if parent != nil && (parent.Kind == ast.KindJsxSelfClosingElement || parent.Kind == ast.KindJsxOpeningElement) { if contextToken.Kind == ast.KindGreaterThanToken { precedingToken := astnav.FindPrecedingToken(file, contextToken.Pos()) if len(parent.TypeArguments()) == 0 || precedingToken != nil && precedingToken.Kind == ast.KindSlashToken { return nil } } return parent } else if parent != nil && parent.Kind == ast.KindJsxAttribute { // Currently we parse JsxOpeningLikeElement as: // JsxOpeningLikeElement // attributes: JsxAttributes // properties: NodeArray return parent.Parent.Parent } // The context token is the closing } or " of an attribute, which means // its parent is a JsxExpression, whose parent is a JsxAttribute, // whose parent is a JsxOpeningLikeElement case ast.KindStringLiteral: if parent != nil && (parent.Kind == ast.KindJsxAttribute || parent.Kind == ast.KindJsxSpreadAttribute) { // Currently we parse JsxOpeningLikeElement as: // JsxOpeningLikeElement // attributes: JsxAttributes // properties: NodeArray return parent.Parent.Parent } case ast.KindCloseBraceToken: if parent != nil && parent.Kind == ast.KindJsxExpression && parent.Parent != nil && parent.Parent.Kind == ast.KindJsxAttribute { // Currently we parse JsxOpeningLikeElement as: // JsxOpeningLikeElement // attributes: JsxAttributes // properties: NodeArray // each JsxAttribute can have initializer as JsxExpression return parent.Parent.Parent.Parent } if parent != nil && parent.Kind == ast.KindJsxSpreadAttribute { // Currently we parse JsxOpeningLikeElement as: // JsxOpeningLikeElement // attributes: JsxAttributes // properties: NodeArray return parent.Parent.Parent } } return nil } // Filters out completion suggestions from 'symbols' according to existing JSX attributes. // @returns Symbols to be suggested in a JSX element, barring those whose attributes // do not occur at the current position and have not otherwise been typed. func filterJsxAttributes( symbols []*ast.Symbol, attributes []*ast.JsxAttributeLike, file *ast.SourceFile, position int, typeChecker *checker.Checker, ) (filteredMembers []*ast.Symbol, spreadMemberNames *collections.Set[string]) { existingNames := collections.Set[string]{} membersDeclaredBySpreadAssignment := collections.Set[string]{} for _, attr := range attributes { // If this is the item we are editing right now, do not filter it out. if isCurrentlyEditingNode(attr, file, position) { continue } if attr.Kind == ast.KindJsxAttribute { existingNames.Add(attr.Name().Text()) } else if ast.IsJsxSpreadAttribute(attr) { setMemberDeclaredBySpreadAssignment(attr, &membersDeclaredBySpreadAssignment, typeChecker) } } return core.Filter(symbols, func(a *ast.Symbol) bool { return !existingNames.Has(a.Name) }), &membersDeclaredBySpreadAssignment } func isTypeKeywordTokenOrIdentifier(node *ast.Node) bool { return ast.IsTypeKeywordToken(node) || ast.IsIdentifier(node) && scanner.IdentifierToKeywordKind(node.AsIdentifier()) == ast.KindTypeKeyword } // Returns the item defaults for completion items, if that capability is supported. // Otherwise, if some item default is not supported by client, sets that property on each item. func (l *LanguageService) setItemDefaults( clientOptions *lsproto.CompletionClientCapabilities, position int, file *ast.SourceFile, items []*lsproto.CompletionItem, defaultCommitCharacters *[]string, optionalReplacementSpan *lsproto.Range, ) *lsproto.CompletionItemDefaults { var itemDefaults *lsproto.CompletionItemDefaults if defaultCommitCharacters != nil { supportsItemCommitCharacters := clientSupportsItemCommitCharacters(clientOptions) if clientSupportsDefaultCommitCharacters(clientOptions) && supportsItemCommitCharacters { itemDefaults = &lsproto.CompletionItemDefaults{ CommitCharacters: defaultCommitCharacters, } } else if supportsItemCommitCharacters { for _, item := range items { if item.CommitCharacters == nil { item.CommitCharacters = defaultCommitCharacters } } } } if optionalReplacementSpan != nil { // Ported from vscode ts extension. insertRange := lsproto.Range{ Start: optionalReplacementSpan.Start, End: l.createLspPosition(position, file), } if clientSupportsDefaultEditRange(clientOptions) { itemDefaults = core.OrElse(itemDefaults, &lsproto.CompletionItemDefaults{}) itemDefaults.EditRange = &lsproto.RangeOrEditRangeWithInsertReplace{ EditRangeWithInsertReplace: &lsproto.EditRangeWithInsertReplace{ Insert: insertRange, Replace: *optionalReplacementSpan, }, } for _, item := range items { // If `editRange` is set, `insertText` is ignored by the client, so we need to // provide `textEdit` instead. if item.InsertText != nil && item.TextEdit == nil { item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{ InsertReplaceEdit: &lsproto.InsertReplaceEdit{ NewText: *item.InsertText, Insert: insertRange, Replace: *optionalReplacementSpan, }, } item.InsertText = nil } } } else if clientSupportsItemInsertReplace(clientOptions) { for _, item := range items { if item.TextEdit == nil { item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{ InsertReplaceEdit: &lsproto.InsertReplaceEdit{ NewText: *core.OrElse(item.InsertText, &item.Label), Insert: insertRange, Replace: *optionalReplacementSpan, }, } } } } } return itemDefaults } func (l *LanguageService) specificKeywordCompletionInfo( clientOptions *lsproto.CompletionClientCapabilities, position int, file *ast.SourceFile, items []*lsproto.CompletionItem, isNewIdentifierLocation bool, optionalReplacementSpan *lsproto.Range, ) *lsproto.CompletionList { defaultCommitCharacters := getDefaultCommitCharacters(isNewIdentifierLocation) itemDefaults := l.setItemDefaults( clientOptions, position, file, items, &defaultCommitCharacters, optionalReplacementSpan, ) return &lsproto.CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, } } func (l *LanguageService) getJsxClosingTagCompletion( location *ast.Node, file *ast.SourceFile, position int, clientOptions *lsproto.CompletionClientCapabilities, ) *lsproto.CompletionList { // We wanna walk up the tree till we find a JSX closing element. jsxClosingElement := ast.FindAncestorOrQuit(location, func(node *ast.Node) ast.FindAncestorResult { switch node.Kind { case ast.KindJsxClosingElement: return ast.FindAncestorTrue case ast.KindLessThanSlashToken, ast.KindGreaterThanToken, ast.KindIdentifier, ast.KindPropertyAccessExpression: return ast.FindAncestorFalse default: return ast.FindAncestorQuit } }) if jsxClosingElement == nil { return nil } // In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag, // instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element. // For example: // var x =
" with type any // And at `
` (with a closing `>`), the completion list will contain "div". // And at property access expressions ` ` the completion will // return full closing tag with an optional replacement span // For example: // var x = // var y = // the completion list at "1" and "2" will contain "MainComponent.Child" with a replacement span of closing tag name hasClosingAngleBracket := findChildOfKind(jsxClosingElement, ast.KindGreaterThanToken, file) != nil tagName := jsxClosingElement.Parent.AsJsxElement().OpeningElement.TagName() closingTag := scanner.GetTextOfNode(tagName) fullClosingTag := closingTag + core.IfElse(hasClosingAngleBracket, "", ">") optionalReplacementSpan := l.createLspRangeFromNode(jsxClosingElement.TagName(), file) defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) item := l.createLSPCompletionItem( fullClosingTag, /*name*/ "", /*insertText*/ "", /*filterText*/ SortTextLocationPriority, ScriptElementKindClassElement, collections.Set[ScriptElementKindModifier]{}, /*kindModifiers*/ nil, /*replacementSpan*/ nil, /*commitCharacters*/ nil, /*labelDetails*/ file, position, clientOptions, true, /*isMemberCompletion*/ false, /*isSnippet*/ false, /*hasAction*/ false, /*preselect*/ "", /*source*/ nil, /*autoImportEntryData*/ // !!! jsx autoimports ) items := []*lsproto.CompletionItem{item} itemDefaults := l.setItemDefaults( clientOptions, position, file, items, &defaultCommitCharacters, optionalReplacementSpan, ) return &lsproto.CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, } } func (l *LanguageService) createLSPCompletionItem( name string, insertText string, filterText string, sortText sortText, elementKind ScriptElementKind, kindModifiers collections.Set[ScriptElementKindModifier], replacementSpan *lsproto.Range, commitCharacters *[]string, labelDetails *lsproto.CompletionItemLabelDetails, file *ast.SourceFile, position int, clientOptions *lsproto.CompletionClientCapabilities, isMemberCompletion bool, isSnippet bool, hasAction bool, preselect bool, source string, autoImportEntryData *completionEntryData, ) *lsproto.CompletionItem { kind := getCompletionsSymbolKind(elementKind) var data any = &itemData{ FileName: file.FileName(), Position: position, Source: source, Name: name, AutoImport: autoImportEntryData, } // Text edit var textEdit *lsproto.TextEditOrInsertReplaceEdit if replacementSpan != nil { textEdit = &lsproto.TextEditOrInsertReplaceEdit{ TextEdit: &lsproto.TextEdit{ NewText: core.IfElse(insertText == "", name, insertText), Range: *replacementSpan, }, } } // Filter text // Ported from vscode ts extension. wordSize, wordStart := getWordLengthAndStart(file, position) dotAccessor := getDotAccessor(file, position-wordSize) if filterText == "" { filterText = getFilterText(file, position, insertText, name, wordStart, dotAccessor) } // Adjustements based on kind modifiers. var tags *[]lsproto.CompletionItemTag var detail *string // Copied from vscode ts extension: `MyCompletionItem.constructor`. if kindModifiers.Has(ScriptElementKindModifierOptional) { if insertText == "" { insertText = name } if filterText == "" { filterText = name } name = name + "?" } if kindModifiers.Has(ScriptElementKindModifierDeprecated) { tags = &[]lsproto.CompletionItemTag{lsproto.CompletionItemTagDeprecated} } if kind == lsproto.CompletionItemKindFile { for _, extensionModifier := range fileExtensionKindModifiers { if kindModifiers.Has(extensionModifier) { if strings.HasSuffix(name, string(extensionModifier)) { detail = ptrTo(name) } else { detail = ptrTo(name + string(extensionModifier)) } break } } } if hasAction && source != "" { // !!! adjust label like vscode does } // Client assumes plain text by default. var insertTextFormat *lsproto.InsertTextFormat if isSnippet { insertTextFormat = ptrTo(lsproto.InsertTextFormatSnippet) } return &lsproto.CompletionItem{ Label: name, LabelDetails: labelDetails, Kind: &kind, Tags: tags, Detail: detail, Preselect: boolToPtr(preselect), SortText: ptrTo(string(sortText)), FilterText: strPtrTo(filterText), InsertText: strPtrTo(insertText), InsertTextFormat: insertTextFormat, TextEdit: textEdit, CommitCharacters: commitCharacters, Data: &data, } } func (l *LanguageService) getLabelCompletionsAtPosition( node *ast.BreakOrContinueStatement, clientOptions *lsproto.CompletionClientCapabilities, file *ast.SourceFile, position int, optionalReplacementSpan *lsproto.Range, ) *lsproto.CompletionList { items := l.getLabelStatementCompletions(node, clientOptions, file, position) if len(items) == 0 { return nil } defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) itemDefaults := l.setItemDefaults( clientOptions, position, file, items, &defaultCommitCharacters, optionalReplacementSpan, ) return &lsproto.CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, } } func (l *LanguageService) getLabelStatementCompletions( node *ast.BreakOrContinueStatement, clientOptions *lsproto.CompletionClientCapabilities, file *ast.SourceFile, position int, ) []*lsproto.CompletionItem { var uniques collections.Set[string] var items []*lsproto.CompletionItem current := node for current != nil { if ast.IsFunctionLike(current) { break } if ast.IsLabeledStatement(current) { name := current.Label().Text() if !uniques.Has(name) { uniques.Add(name) items = append(items, l.createLSPCompletionItem( name, "", /*insertText*/ "", /*filterText*/ SortTextLocationPriority, ScriptElementKindLabel, collections.Set[ScriptElementKindModifier]{}, /*kindModifiers*/ nil, /*replacementSpan*/ nil, /*commitCharacters*/ nil, /*labelDetails*/ file, position, clientOptions, false, /*isMemberCompletion*/ false, /*isSnippet*/ false, /*hasAction*/ false, /*preselect*/ "", /*source*/ nil, /*autoImportEntryData*/ )) } } current = current.Parent } return items } func isCompletionListBlocker( contextToken *ast.Node, previousToken *ast.Node, location *ast.Node, file *ast.SourceFile, position int, typeChecker *checker.Checker, ) bool { return isInStringOrRegularExpressionOrTemplateLiteral(contextToken, position) || isSolelyIdentifierDefinitionLocation(contextToken, previousToken, file, position, typeChecker) || isDotOfNumericLiteral(contextToken, file) || isInJsxText(contextToken, location) || ast.IsBigIntLiteral(contextToken) } func isInStringOrRegularExpressionOrTemplateLiteral(contextToken *ast.Node, position int) bool { // To be "in" one of these literals, the position has to be: // 1. entirely within the token text. // 2. at the end position of an unterminated token. // 3. at the end of a regular expression (due to trailing flags like '/foo/g'). return (ast.IsRegularExpressionLiteral(contextToken) || ast.IsStringTextContainingNode(contextToken)) && (contextToken.Loc.ContainsExclusive(position)) || position == contextToken.End() && (ast.IsUnterminatedLiteral(contextToken) || ast.IsRegularExpressionLiteral(contextToken)) } // true if we are certain that the currently edited location must define a new location; false otherwise. func isSolelyIdentifierDefinitionLocation( contextToken *ast.Node, previousToken *ast.Node, file *ast.SourceFile, position int, typeChecker *checker.Checker, ) bool { parent := contextToken.Parent containingNodeKind := parent.Kind switch contextToken.Kind { case ast.KindCommaToken: return containingNodeKind == ast.KindVariableDeclaration || isVariableDeclarationListButNotTypeArgument(contextToken, file, typeChecker) || containingNodeKind == ast.KindVariableStatement || containingNodeKind == ast.KindEnumDeclaration || // enum a { foo, | isFunctionLikeButNotConstructor(containingNodeKind) || containingNodeKind == ast.KindInterfaceDeclaration || // interface A= contextToken.Pos()) case ast.KindDotToken: return containingNodeKind == ast.KindArrayBindingPattern // var [.| case ast.KindColonToken: return containingNodeKind == ast.KindBindingElement // var {x :html| case ast.KindOpenBracketToken: return containingNodeKind == ast.KindArrayBindingPattern // var [x| case ast.KindOpenParenToken: return containingNodeKind == ast.KindCatchClause || isFunctionLikeButNotConstructor(containingNodeKind) case ast.KindOpenBraceToken: return containingNodeKind == ast.KindEnumDeclaration // enum a { | case ast.KindLessThanToken: return containingNodeKind == ast.KindClassDeclaration || // class A< | containingNodeKind == ast.KindClassExpression || // var C = class D< | containingNodeKind == ast.KindInterfaceDeclaration || // interface A< | containingNodeKind == ast.KindTypeAliasDeclaration || // type List< | ast.IsFunctionLikeKind(containingNodeKind) case ast.KindStaticKeyword: return containingNodeKind == ast.KindPropertyDeclaration && !ast.IsClassLike(parent.Parent) case ast.KindDotDotDotToken: return containingNodeKind == ast.KindParameter || (parent.Parent != nil && parent.Parent.Kind == ast.KindArrayBindingPattern) // var [...z| case ast.KindPublicKeyword, ast.KindPrivateKeyword, ast.KindProtectedKeyword: return containingNodeKind == ast.KindParameter && !ast.IsConstructorDeclaration(parent.Parent) case ast.KindAsKeyword: return containingNodeKind == ast.KindImportSpecifier || containingNodeKind == ast.KindExportSpecifier || containingNodeKind == ast.KindNamespaceImport case ast.KindGetKeyword, ast.KindSetKeyword: return !isFromObjectTypeDeclaration(contextToken) case ast.KindIdentifier: if (containingNodeKind == ast.KindImportSpecifier || containingNodeKind == ast.KindExportSpecifier) && contextToken == parent.Name() && contextToken.Text() == "type" { // import { type | } return false } ancestorVariableDeclaration := ast.FindAncestor(parent, ast.IsVariableDeclaration) if ancestorVariableDeclaration != nil && getLineEndOfPosition(file, contextToken.End()) < position { // let a // | return false } case ast.KindClassKeyword, ast.KindEnumKeyword, ast.KindInterfaceKeyword, ast.KindFunctionKeyword, ast.KindVarKeyword, ast.KindImportKeyword, ast.KindLetKeyword, ast.KindConstKeyword, ast.KindInferKeyword: return true case ast.KindTypeKeyword: // import { type foo| } return containingNodeKind != ast.KindImportSpecifier case ast.KindAsteriskToken: return ast.IsFunctionLike(parent) && !ast.IsMethodDeclaration(parent) } // If the previous token is keyword corresponding to class member completion keyword // there will be completion available here if isClassMemberCompletionKeyword(keywordForNode(contextToken)) && isFromObjectTypeDeclaration(contextToken) { return false } if isConstructorParameterCompletion(contextToken) { // constructor parameter completion is available only if // - its modifier of the constructor parameter or // - its name of the parameter and not being edited // eg. constructor(a |<- this shouldnt show completion if !ast.IsIdentifier(contextToken) || ast.IsParameterPropertyModifier(keywordForNode(contextToken)) || isCurrentlyEditingNode(contextToken, file, position) { return false } } // Previous token may have been a keyword that was converted to an identifier. switch keywordForNode(contextToken) { case ast.KindAbstractKeyword, ast.KindClassKeyword, ast.KindConstKeyword, ast.KindDeclareKeyword, ast.KindEnumKeyword, ast.KindFunctionKeyword, ast.KindInterfaceKeyword, ast.KindLetKeyword, ast.KindPrivateKeyword, ast.KindProtectedKeyword, ast.KindPublicKeyword, ast.KindStaticKeyword, ast.KindVarKeyword: return true case ast.KindAsyncKeyword: return ast.IsPropertyDeclaration(contextToken.Parent) } // If we are inside a class declaration, and `constructor` is totally not present, // but we request a completion manually at a whitespace... ancestorClassLike := ast.FindAncestor(parent, ast.IsClassLike) if ancestorClassLike != nil && contextToken == previousToken && isPreviousPropertyDeclarationTerminated(contextToken, file, position) { // Don't block completions. return false } ancestorPropertyDeclaration := ast.FindAncestor(parent, ast.IsPropertyDeclaration) // If we are inside a class declaration and typing `constructor` after property declaration... if ancestorPropertyDeclaration != nil && contextToken != previousToken && ast.IsClassLike(previousToken.Parent.Parent) && // And the cursor is at the token... position <= previousToken.End() { // If we are sure that the previous property declaration is terminated according to newline or semicolon... if isPreviousPropertyDeclarationTerminated(contextToken, file, previousToken.End()) { // Don't block completions. return false } else if contextToken.Kind != ast.KindEqualsToken && // Should not block: `class C { blah = c/**/ }` // But should block: `class C { blah = somewhat c/**/ }` and `class C { blah: SomeType c/**/ }` (ast.IsInitializedProperty(ancestorPropertyDeclaration) || ancestorPropertyDeclaration.Type() != nil) { return true } } return ast.IsDeclarationName(contextToken) && !ast.IsShorthandPropertyAssignment(parent) && !ast.IsJsxAttribute(parent) && // Don't block completions if we're in `class C /**/`, `interface I /**/` or `` , // because we're *past* the end of the identifier and might want to complete `extends`. // If `contextToken !== previousToken`, this is `class C ex/**/`, `interface I ex/**/` or ``. !((ast.IsClassLike(parent) || ast.IsInterfaceDeclaration(parent) || ast.IsTypeParameterDeclaration(parent)) && (contextToken != previousToken || position > previousToken.End())) } func isVariableDeclarationListButNotTypeArgument(node *ast.Node, file *ast.SourceFile, typeChecker *checker.Checker) bool { return node.Parent.Kind == ast.KindVariableDeclarationList && !isPossiblyTypeArgumentPosition(node, file, typeChecker) } func isFunctionLikeButNotConstructor(kind ast.Kind) bool { return ast.IsFunctionLikeKind(kind) && kind != ast.KindConstructor } func isPreviousPropertyDeclarationTerminated(contextToken *ast.Node, file *ast.SourceFile, position int) bool { return contextToken.Kind != ast.KindEqualsToken && (contextToken.Kind == ast.KindSemicolonToken || getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position)) } func isDotOfNumericLiteral(contextToken *ast.Node, file *ast.SourceFile) bool { if contextToken.Kind == ast.KindNumericLiteral { text := file.Text()[contextToken.Pos():contextToken.End()] r, _ := utf8.DecodeLastRuneInString(text) return r == '.' } return false } func isInJsxText(contextToken *ast.Node, location *ast.Node) bool { if contextToken.Kind == ast.KindJsxText { return true } if contextToken.Kind == ast.KindGreaterThanToken && contextToken.Parent != nil { // /**/ /> // /**/ > // - contextToken: GreaterThanToken (before cursor) // - location: JsxSelfClosingElement or JsxOpeningElement // - contextToken.parent === location if location == contextToken.Parent && ast.IsJsxOpeningLikeElement(location) { return false } if contextToken.Parent.Kind == ast.KindJsxOpeningElement { //
/**/ // - contextToken: GreaterThanToken (before cursor) // - location: JSXElement // - different parents (JSXOpeningElement, JSXElement) return location.Parent.Kind != ast.KindJsxOpeningElement } if contextToken.Parent.Kind == ast.KindJsxClosingElement || contextToken.Parent.Kind == ast.KindJsxSelfClosingElement { return contextToken.Parent.Parent != nil && contextToken.Parent.Parent.Kind == ast.KindJsxElement } } return false } func hasCompletionItem(clientOptions *lsproto.CompletionClientCapabilities) bool { return clientOptions != nil && clientOptions.CompletionItem != nil } func clientSupportsItemLabelDetails(clientOptions *lsproto.CompletionClientCapabilities) bool { return hasCompletionItem(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.LabelDetailsSupport) } func clientSupportsItemSnippet(clientOptions *lsproto.CompletionClientCapabilities) bool { return hasCompletionItem(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.SnippetSupport) } func clientSupportsItemCommitCharacters(clientOptions *lsproto.CompletionClientCapabilities) bool { return hasCompletionItem(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.CommitCharactersSupport) } func clientSupportsItemInsertReplace(clientOptions *lsproto.CompletionClientCapabilities) bool { return hasCompletionItem(clientOptions) && ptrIsTrue(clientOptions.CompletionItem.InsertReplaceSupport) } func clientSupportsDefaultCommitCharacters(clientOptions *lsproto.CompletionClientCapabilities) bool { if clientOptions == nil || clientOptions.CompletionList == nil || clientOptions.CompletionList.ItemDefaults == nil { return false } return slices.Contains(*clientOptions.CompletionList.ItemDefaults, "commitCharacters") } func clientSupportsDefaultEditRange(clientOptions *lsproto.CompletionClientCapabilities) bool { if clientOptions == nil || clientOptions.CompletionList == nil || clientOptions.CompletionList.ItemDefaults == nil { return false } return slices.Contains(*clientOptions.CompletionList.ItemDefaults, "editRange") } type argumentInfoForCompletions struct { invocation *ast.CallLikeExpression argumentIndex int argumentCount int } func getArgumentInfoForCompletions(node *ast.Node, position int, file *ast.SourceFile, typeChecker *checker.Checker) *argumentInfoForCompletions { info := getImmediatelyContainingArgumentInfo(node, position, file, typeChecker) if info == nil || info.isTypeParameterList || info.invocation.callInvocation == nil { return nil } return &argumentInfoForCompletions{ invocation: info.invocation.callInvocation.node, argumentIndex: info.argumentIndex, argumentCount: info.argumentCount, } } type itemData struct { FileName string `json:"fileName"` Position int `json:"position"` Source string `json:"source,omitempty"` Name string `json:"name,omitempty"` AutoImport *completionEntryData `json:"autoImport,omitempty"` } type completionEntryData struct { /** * The name of the property or export in the module's symbol table. Differs from the completion name * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. */ ExportName string `json:"exportName"` ExportMapKey ExportInfoMapKey `json:"exportMapKey"` ModuleSpecifier string `json:"moduleSpecifier"` /** The file name declaring the export's module symbol, if it was an external module */ FileName *string `json:"fileName"` /** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */ AmbientModuleName *string `json:"ambientModuleName"` /** True if the export was found in the package.json AutoImportProvider */ IsPackageJsonImport core.Tristate `json:"isPackageJsonImport"` } func (d *completionEntryData) toSymbolOriginExport(symbolName string, moduleSymbol *ast.Symbol, isDefaultExport bool) *symbolOriginInfoExport { return &symbolOriginInfoExport{ symbolName: symbolName, moduleSymbol: moduleSymbol, exportName: d.ExportName, exportMapKey: d.ExportMapKey, moduleSpecifier: d.ModuleSpecifier, } } // Special values for `CompletionInfo['source']` used to disambiguate // completion items with the same `name`. (Each completion item must // have a unique name/source combination, because those two fields // comprise `CompletionEntryIdentifier` in `getCompletionEntryDetails`. // // When the completion item is an auto-import suggestion, the source // is the module specifier of the suggestion. To avoid collisions, // the values here should not be a module specifier we would ever // generate for an auto-import. const ( // Completions that require `this.` insertion text SourceThisProperty = "ThisProperty/" // Auto-import that comes attached to a class member snippet SourceClassMemberSnippet = "ClassMemberSnippet/" // A type-only import that needs to be promoted in order to be used at the completion location SourceTypeOnlyAlias = "TypeOnlyAlias/" // Auto-import that comes attached to an object literal method snippet SourceObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/" // Case completions for switch statements SourceSwitchCases = "SwitchCases/" // Completions for an object literal expression SourceObjectLiteralMemberWithComma = "ObjectLiteralMemberWithComma/" ) func (l *LanguageService) ResolveCompletionItem( ctx context.Context, item *lsproto.CompletionItem, data *itemData, clientOptions *lsproto.CompletionClientCapabilities, preferences *UserPreferences, ) (*lsproto.CompletionItem, error) { if data == nil { return nil, errors.New("completion item data is nil") } program, file := l.tryGetProgramAndFile(data.FileName) if file == nil { return nil, fmt.Errorf("file not found: %s", data.FileName) } return l.getCompletionItemDetails(ctx, program, data.Position, file, item, data, clientOptions, preferences), nil } func GetCompletionItemData(item *lsproto.CompletionItem) (*itemData, error) { bytes, err := json.Marshal(item.Data) if err != nil { return nil, fmt.Errorf("failed to marshal completion item data: %w", err) } var itemData itemData if err := json.Unmarshal(bytes, &itemData); err != nil { return nil, fmt.Errorf("failed to unmarshal completion item data: %w", err) } return &itemData, nil } func (l *LanguageService) getCompletionItemDetails( ctx context.Context, program *compiler.Program, position int, file *ast.SourceFile, item *lsproto.CompletionItem, itemData *itemData, clientOptions *lsproto.CompletionClientCapabilities, preferences *UserPreferences, ) *lsproto.CompletionItem { checker, done := program.GetTypeCheckerForFile(ctx, file) defer done() contextToken, previousToken := getRelevantTokens(position, file) if IsInString(file, position, previousToken) { return l.getStringLiteralCompletionDetails( ctx, checker, item, itemData.Name, file, position, contextToken, preferences, ) } // Compute all the completion symbols again. symbolCompletion := l.getSymbolCompletionFromItemData( ctx, checker, file, position, itemData, clientOptions, preferences, ) switch { case symbolCompletion.request != nil: request := *symbolCompletion.request switch request := request.(type) { case *completionDataJSDocTagName: return createSimpleDetails(item, itemData.Name) case *completionDataJSDocTag: return createSimpleDetails(item, itemData.Name) case *completionDataJSDocParameterName: return createSimpleDetails(item, itemData.Name) case *completionDataKeyword: if core.Some(request.keywordCompletions, func(c *lsproto.CompletionItem) bool { return c.Label == itemData.Name }) { return createSimpleDetails(item, itemData.Name) } return item default: panic(fmt.Sprintf("Unexpected completion data type: %T", request)) } case symbolCompletion.symbol != nil: symbolDetails := symbolCompletion.symbol actions := l.getCompletionItemActions(ctx, checker, file, position, itemData, symbolDetails, preferences) return createCompletionDetailsForSymbol( item, symbolDetails.symbol, checker, symbolDetails.location, actions, ) case symbolCompletion.literal != nil: literal := symbolCompletion.literal return createSimpleDetails(item, completionNameForLiteral(file, preferences, *literal)) case symbolCompletion.cases != nil: // !!! exhaustive case completions return item default: // Didn't find a symbol with this name. See if we can find a keyword instead. if core.Some(allKeywordCompletions(), func(c *lsproto.CompletionItem) bool { return c.Label == itemData.Name }) { return createSimpleDetails(item, itemData.Name) } return item } } type detailsData struct { symbol *symbolDetails request *completionData literal *literalValue cases *struct{} } type symbolDetails struct { symbol *ast.Symbol location *ast.Node origin *symbolOriginInfo previousToken *ast.Node contextToken *ast.Node jsxInitializer jsxInitializer isTypeOnlyLocation bool } func (l *LanguageService) getSymbolCompletionFromItemData( ctx context.Context, ch *checker.Checker, file *ast.SourceFile, position int, itemData *itemData, clientOptions *lsproto.CompletionClientCapabilities, preferences *UserPreferences, ) detailsData { if itemData.Source == SourceSwitchCases { return detailsData{ cases: &struct{}{}, } } if itemData.AutoImport != nil { if autoImportSymbolData := l.getAutoImportSymbolFromCompletionEntryData(ch, itemData.AutoImport.ExportName, itemData.AutoImport); autoImportSymbolData != nil { autoImportSymbolData.contextToken, autoImportSymbolData.previousToken = getRelevantTokens(position, file) autoImportSymbolData.location = astnav.GetTouchingPropertyName(file, position) autoImportSymbolData.jsxInitializer = jsxInitializer{false, nil} autoImportSymbolData.isTypeOnlyLocation = false return detailsData{symbol: autoImportSymbolData} } } completionData := l.getCompletionData(ctx, ch, file, position, &UserPreferences{IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue}) if completionData == nil { return detailsData{} } if _, ok := completionData.(*completionDataData); !ok { return detailsData{ request: &completionData, } } data := completionData.(*completionDataData) var literal literalValue for _, l := range data.literals { if completionNameForLiteral(file, preferences, l) == itemData.Name { literal = l break } } if literal != nil { return detailsData{ literal: &literal, } } // Find the symbol with the matching entry name. // We don't need to perform character checks here because we're only comparing the // name against 'entryName' (which is known to be good), not building a new // completion entry. for _, symbol := range data.symbols { symbolId := ast.GetSymbolId(symbol) origin := data.symbolToOriginInfoMap[symbolId] displayName, _ := getCompletionEntryDisplayNameForSymbol(symbol, origin, data.completionKind, data.isJsxIdentifierExpected) if displayName == itemData.Name && (itemData.Source == string(completionSourceClassMemberSnippet) && symbol.Flags&ast.SymbolFlagsClassMember != 0 || itemData.Source == string(completionSourceObjectLiteralMethodSnippet) && symbol.Flags&(ast.SymbolFlagsProperty|ast.SymbolFlagsMethod) != 0 || getSourceFromOrigin(origin) == itemData.Source || itemData.Source == string(completionSourceObjectLiteralMemberWithComma)) { return detailsData{ symbol: &symbolDetails{ symbol: symbol, location: data.location, origin: origin, previousToken: data.previousToken, contextToken: data.contextToken, jsxInitializer: data.jsxInitializer, isTypeOnlyLocation: data.isTypeOnlyLocation, }, } } } return detailsData{} } func (l *LanguageService) getAutoImportSymbolFromCompletionEntryData(ch *checker.Checker, name string, autoImportData *completionEntryData) *symbolDetails { containingProgram := l.GetProgram() // !!! isPackageJson ? packageJsonAutoimportProvider : program var moduleSymbol *ast.Symbol if autoImportData.AmbientModuleName != nil { moduleSymbol = ch.TryFindAmbientModule(*autoImportData.AmbientModuleName) } else if autoImportData.FileName != nil { moduleSymbolSourceFile := containingProgram.GetSourceFile(*autoImportData.FileName) if moduleSymbolSourceFile == nil { panic("module sourceFile not found: " + *autoImportData.FileName) } moduleSymbol = ch.GetMergedSymbol(moduleSymbolSourceFile.Symbol) } if moduleSymbol == nil { return nil } var symbol *ast.Symbol if autoImportData.ExportName == ast.InternalSymbolNameExportEquals { symbol = ch.ResolveExternalModuleSymbol(moduleSymbol) } else { symbol = ch.TryGetMemberInModuleExportsAndProperties(autoImportData.ExportName, moduleSymbol) } if symbol == nil { return nil } isDefaultExport := autoImportData.ExportName == ast.InternalSymbolNameDefault if isDefaultExport { if localSymbol := binder.GetLocalSymbolForExportDefault(symbol); localSymbol != nil { symbol = localSymbol } } origin := &symbolOriginInfo{ kind: symbolOriginInfoKindExport, fileName: *autoImportData.FileName, isFromPackageJson: autoImportData.IsPackageJsonImport.IsTrue(), isDefaultExport: isDefaultExport, data: autoImportData.toSymbolOriginExport(name, moduleSymbol, isDefaultExport), } return &symbolDetails{symbol: symbol, origin: origin} } func createSimpleDetails( item *lsproto.CompletionItem, name string, ) *lsproto.CompletionItem { return createCompletionDetails(item, name, "" /*documentation*/) } func createCompletionDetails( item *lsproto.CompletionItem, detail string, documentation string, ) *lsproto.CompletionItem { // !!! fill in additionalTextEdits from code actions if item.Detail == nil && detail != "" { item.Detail = &detail } if documentation != "" { item.Documentation = &lsproto.StringOrMarkupContent{ MarkupContent: &lsproto.MarkupContent{ Kind: lsproto.MarkupKindMarkdown, Value: documentation, }, } } return item } type codeAction struct { // Description of the code action to display in the UI of the editor description string // Text changes to apply to each file as part of the code action changes []*lsproto.TextEdit } func createCompletionDetailsForSymbol( item *lsproto.CompletionItem, symbol *ast.Symbol, checker *checker.Checker, location *ast.Node, actions []codeAction, ) *lsproto.CompletionItem { details := make([]string, 0, len(actions)+1) edits := make([]*lsproto.TextEdit, 0, len(actions)) for _, action := range actions { details = append(details, action.description) edits = append(edits, action.changes...) } quickInfo, documentation := getQuickInfoAndDocumentationForSymbol(checker, symbol, location) details = append(details, quickInfo) if len(edits) != 0 { item.AdditionalTextEdits = &edits } return createCompletionDetails(item, strings.Join(details, "\n\n"), documentation) } // !!! snippets func (l *LanguageService) getCompletionItemActions(ctx context.Context, ch *checker.Checker, file *ast.SourceFile, position int, itemData *itemData, symbolDetails *symbolDetails, preferences *UserPreferences) []codeAction { if itemData.AutoImport != nil && itemData.AutoImport.ModuleSpecifier != "" && symbolDetails.previousToken != nil { // Import statement completion: 'import c|' if symbolDetails.contextToken != nil && l.getImportStatementCompletionInfo(symbolDetails.contextToken, file).replacementSpan != nil { return nil } else if l.getImportStatementCompletionInfo(symbolDetails.previousToken, file).replacementSpan != nil { return nil // !!! sourceDisplay [textPart(data.moduleSpecifier)] } } // !!! CompletionSource.ClassMemberSnippet // !!! origin.isTypeOnlyAlias // entryId.source == CompletionSourceObjectLiteralMemberWithComma && contextToken if symbolDetails.origin == nil { return nil } symbol := symbolDetails.symbol if symbol.ExportSymbol != nil { symbol = symbol.ExportSymbol } targetSymbol := ch.GetMergedSymbol(ch.SkipAlias(symbol)) isJsxOpeningTagName := symbolDetails.contextToken != nil && symbolDetails.contextToken.Kind == ast.KindLessThanToken && ast.IsJsxOpeningLikeElement(symbolDetails.contextToken.Parent) if symbolDetails.previousToken != nil && ast.IsIdentifier(symbolDetails.previousToken) { // If the previous token is an identifier, we can use its start position. position = astnav.GetStartOfNode(symbolDetails.previousToken, file, false) } moduleSymbol := symbolDetails.origin.moduleSymbol() var exportMapkey ExportInfoMapKey if itemData.AutoImport != nil { exportMapkey = itemData.AutoImport.ExportMapKey } moduleSpecifier, importCompletionAction := l.getImportCompletionAction( ctx, ch, targetSymbol, moduleSymbol, file, position, exportMapkey, itemData.Name, isJsxOpeningTagName, // formatContext, preferences, ) if !(moduleSpecifier == itemData.AutoImport.ModuleSpecifier || itemData.AutoImport.ModuleSpecifier == "") { panic("") } return []codeAction{importCompletionAction} } func (l *LanguageService) getImportStatementCompletionInfo(contextToken *ast.Node, sourceFile *ast.SourceFile) importStatementCompletionInfo { result := importStatementCompletionInfo{} var candidate *ast.Node parent := contextToken.Parent switch { case ast.IsImportEqualsDeclaration(parent): // import Foo | // import Foo f| lastToken := lsutil.GetLastToken(parent, sourceFile) if contextToken.Kind == ast.KindIdentifier && lastToken != contextToken { result.keywordCompletion = ast.KindFromKeyword result.isKeywordOnlyCompletion = true } else { if contextToken.Kind != ast.KindTypeKeyword { result.keywordCompletion = ast.KindTypeKeyword } if isModuleSpecifierMissingOrEmpty(parent.AsImportEqualsDeclaration().ModuleReference) { candidate = parent } } case couldBeTypeOnlyImportSpecifier(parent, contextToken) && canCompleteFromNamedBindings(parent.Parent): candidate = parent case ast.IsNamedImports(parent) || ast.IsNamespaceImport(parent): if !parent.Parent.IsTypeOnly() && (contextToken.Kind == ast.KindOpenBraceToken || contextToken.Kind == ast.KindImportKeyword || contextToken.Kind == ast.KindCommaToken) { result.keywordCompletion = ast.KindTypeKeyword } if canCompleteFromNamedBindings(parent) { // At `import { ... } |` or `import * as Foo |`, the only possible completion is `from` if contextToken.Kind == ast.KindCloseBraceToken || contextToken.Kind == ast.KindIdentifier { result.isKeywordOnlyCompletion = true result.keywordCompletion = ast.KindFromKeyword } else { candidate = parent.Parent.Parent } } case ast.IsExportDeclaration(parent) && contextToken.Kind == ast.KindAsteriskToken, ast.IsNamedExports(parent) && contextToken.Kind == ast.KindCloseBraceToken: result.isKeywordOnlyCompletion = true result.keywordCompletion = ast.KindFromKeyword case contextToken.Kind == ast.KindImportKeyword: if ast.IsSourceFile(parent) { // A lone import keyword with nothing following it does not parse as a statement at all result.keywordCompletion = ast.KindTypeKeyword candidate = contextToken } else if ast.IsImportDeclaration(parent) { // `import s| from` result.keywordCompletion = ast.KindTypeKeyword if isModuleSpecifierMissingOrEmpty(parent.ModuleSpecifier()) { candidate = parent } } } if candidate != nil { result.isNewIdentifierLocation = true result.replacementSpan = l.getSingleLineReplacementSpanForImportCompletionNode(candidate) result.couldBeTypeOnlyImportSpecifier = couldBeTypeOnlyImportSpecifier(candidate, contextToken) if ast.IsImportDeclaration(candidate) { result.isTopLevelTypeOnly = candidate.AsImportDeclaration().ImportClause.IsTypeOnly() } else if candidate.Kind == ast.KindImportEqualsDeclaration { result.isTopLevelTypeOnly = candidate.IsTypeOnly() } } else { result.isNewIdentifierLocation = result.keywordCompletion == ast.KindTypeKeyword } return result } func (l *LanguageService) getSingleLineReplacementSpanForImportCompletionNode(node *ast.Node) *lsproto.Range { // node is ImportDeclaration | ImportEqualsDeclaration | ImportSpecifier | JSDocImportTag | Token if ancestor := ast.FindAncestor(node, core.Or(ast.IsImportDeclaration, ast.IsImportEqualsDeclaration, ast.IsJSDocImportTag)); ancestor != nil { node = ancestor } sourceFile := ast.GetSourceFileOfNode(node) if printer.GetLinesBetweenPositions(sourceFile, node.Pos(), node.End()) == 0 { return l.createLspRangeFromNode(node, sourceFile) } if node.Kind == ast.KindImportKeyword || node.Kind == ast.KindImportSpecifier { panic("ImportKeyword was necessarily on one line; ImportSpecifier was necessarily parented in an ImportDeclaration") } // Guess which point in the import might actually be a later statement parsed as part of the import // during parser recovery - either in the middle of named imports, or the module specifier. var potentialSplitPoint *ast.Node if node.Kind == ast.KindImportDeclaration || node.Kind == ast.KindJSDocImportTag { var specifier *ast.Node if importClause := node.ImportClause(); importClause != nil { specifier = getPotentiallyInvalidImportSpecifier(importClause.AsImportClause().NamedBindings) } if specifier != nil { potentialSplitPoint = specifier } else { potentialSplitPoint = node.ModuleSpecifier() } } else { potentialSplitPoint = node.AsImportEqualsDeclaration().ModuleReference } withoutModuleSpecifier := core.NewTextRange(scanner.GetTokenPosOfNode(lsutil.GetFirstToken(node, sourceFile), sourceFile, false), potentialSplitPoint.Pos()) // The module specifier/reference was previously found to be missing, empty, or // not a string literal - in this last case, it's likely that statement on a following // line was parsed as the module specifier of a partially-typed import, e.g. // import Foo| // interface Blah {} // This appears to be a multiline-import, and editors can't replace multiple lines. // But if everything but the "module specifier" is on one line, by this point we can // assume that the "module specifier" is actually just another statement, and return // the single-line range of the import excluding that probable statement. if printer.GetLinesBetweenPositions(sourceFile, withoutModuleSpecifier.Pos(), withoutModuleSpecifier.End()) == 0 { return l.createLspRangeFromBounds(withoutModuleSpecifier.Pos(), withoutModuleSpecifier.End(), sourceFile) } return nil } func couldBeTypeOnlyImportSpecifier(importSpecifier *ast.Node, contextToken *ast.Node) bool { return ast.IsImportSpecifier(importSpecifier) && (importSpecifier.IsTypeOnly() || contextToken == importSpecifier.Name() && isTypeKeywordTokenOrIdentifier(contextToken)) } func canCompleteFromNamedBindings(namedBindings *ast.NamedImportBindings) bool { if !isModuleSpecifierMissingOrEmpty(namedBindings.Parent.Parent.ModuleSpecifier()) || namedBindings.Parent.Name() != nil { return false } if ast.IsNamedImports(namedBindings) { // We can only complete on named imports if there are no other named imports already, // but parser recovery sometimes puts later statements in the named imports list, so // we try to only consider the probably-valid ones. invalidNamedImport := getPotentiallyInvalidImportSpecifier(namedBindings) elements := namedBindings.Elements() validImports := len(elements) if invalidNamedImport != nil { validImports = slices.Index(elements, invalidNamedImport) } return validImports < 2 && validImports > -1 } return true } // Tries to identify the first named import that is not really a named import, but rather // just parser recovery for a situation like: // // import { Foo| // interface Bar {} // // in which `Foo`, `interface`, and `Bar` are all parsed as import specifiers. The caller // will also check if this token is on a separate line from the rest of the import. func getPotentiallyInvalidImportSpecifier(namedBindings *ast.NamedImportBindings) *ast.Node { if namedBindings.Kind != ast.KindNamedImports { return nil } return core.Find(namedBindings.Elements(), func(e *ast.Node) bool { return e.PropertyName() == nil && isNonContextualKeyword(scanner.StringToToken(e.Name().Text())) && astnav.FindPrecedingToken(ast.GetSourceFileOfNode(namedBindings), e.Name().Pos()).Kind != ast.KindCommaToken }) } func isModuleSpecifierMissingOrEmpty(specifier *ast.Expression) bool { if ast.NodeIsMissing(specifier) { return true } node := specifier if ast.IsExternalModuleReference(node) { node = node.Expression() } if !ast.IsStringLiteralLike(node) { return true } return node.Text() == "" } func hasDocComment(file *ast.SourceFile, position int) bool { token := astnav.GetTokenAtPosition(file, position) return ast.FindAncestor(token, (*ast.Node).IsJSDoc) != nil } // Get the corresponding JSDocTag node if the position is in a JSDoc comment func getJSDocTagAtPosition(node *ast.Node, position int) *ast.JSDocTag { return ast.FindAncestorOrQuit(node, func(n *ast.Node) ast.FindAncestorResult { if ast.IsJSDocTag(n) && n.Loc.ContainsInclusive(position) { return ast.FindAncestorTrue } if n.IsJSDoc() { return ast.FindAncestorQuit } return ast.FindAncestorFalse }) } func tryGetTypeExpressionFromTag(tag *ast.JSDocTag) *ast.Node { if isTagWithTypeExpression(tag) { var typeExpression *ast.Node if ast.IsJSDocTemplateTag(tag) { typeExpression = tag.AsJSDocTemplateTag().Constraint } else { typeExpression = tag.TypeExpression() } if typeExpression != nil && typeExpression.Kind == ast.KindJSDocTypeExpression { return typeExpression } } if ast.IsJSDocAugmentsTag(tag) || ast.IsJSDocImplementsTag(tag) { return tag.ClassName() } return nil } func isTagWithTypeExpression(tag *ast.JSDocTag) bool { switch tag.Kind { case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag, ast.KindJSDocReturnTag, ast.KindJSDocTypeTag, ast.KindJSDocTypedefTag, ast.KindJSDocSatisfiesTag: return true case ast.KindJSDocTemplateTag: return tag.AsJSDocTemplateTag().Constraint != nil default: return false } } func (l *LanguageService) jsDocCompletionInfo( clientOptions *lsproto.CompletionClientCapabilities, position int, file *ast.SourceFile, items []*lsproto.CompletionItem, ) *lsproto.CompletionList { defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) itemDefaults := l.setItemDefaults( clientOptions, position, file, items, &defaultCommitCharacters, nil, /*optionalReplacementSpan*/ ) return &lsproto.CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, } } var jsDocTagNames = []string{ "abstract", "access", "alias", "argument", "async", "augments", "author", "borrows", "callback", "class", "classdesc", "constant", "constructor", "constructs", "copyright", "default", "deprecated", "description", "emits", "enum", "event", "example", "exports", "extends", "external", "field", "file", "fileoverview", "fires", "function", "generator", "global", "hideconstructor", "host", "ignore", "implements", "import", "inheritdoc", "inner", "instance", "interface", "kind", "lends", "license", "link", "linkcode", "linkplain", "listens", "member", "memberof", "method", "mixes", "module", "name", "namespace", "overload", "override", "package", "param", "private", "prop", "property", "protected", "public", "readonly", "requires", "returns", "satisfies", "see", "since", "static", "summary", "template", "this", "throws", "todo", "tutorial", "type", "typedef", "var", "variation", "version", "virtual", "yields", } var jsDocTagNameCompletionItems = sync.OnceValue(func() []*lsproto.CompletionItem { items := make([]*lsproto.CompletionItem, 0, len(jsDocTagNames)) for _, tagName := range jsDocTagNames { item := &lsproto.CompletionItem{ Label: tagName, Kind: ptrTo(lsproto.CompletionItemKindKeyword), SortText: ptrTo(string(SortTextLocationPriority)), } items = append(items, item) } return items }) var jsDocTagCompletionItems = sync.OnceValue(func() []*lsproto.CompletionItem { items := make([]*lsproto.CompletionItem, 0, len(jsDocTagNames)) for _, tagName := range jsDocTagNames { item := &lsproto.CompletionItem{ Label: "@" + tagName, Kind: ptrTo(lsproto.CompletionItemKindKeyword), SortText: ptrTo(string(SortTextLocationPriority)), } items = append(items, item) } return items }) func getJSDocTagNameCompletions() []*lsproto.CompletionItem { return cloneItems(jsDocTagNameCompletionItems()) } func getJSDocTagCompletions() []*lsproto.CompletionItem { return cloneItems(jsDocTagCompletionItems()) } func getJSDocParameterCompletions( clientOptions *lsproto.CompletionClientCapabilities, file *ast.SourceFile, position int, typeChecker *checker.Checker, options *core.CompilerOptions, preferences *UserPreferences, tagNameOnly bool, ) []*lsproto.CompletionItem { currentToken := astnav.GetTokenAtPosition(file, position) if !ast.IsJSDocTag(currentToken) && !currentToken.IsJSDoc() { return nil } var jsDoc *ast.JSDocNode if currentToken.IsJSDoc() { jsDoc = currentToken } else { jsDoc = currentToken.Parent } if !jsDoc.IsJSDoc() { return nil } fun := jsDoc.Parent if !ast.IsFunctionLike(fun) { return nil } isJS := ast.IsSourceFileJS(file) // isSnippet := clientSupportsItemSnippet(clientOptions) isSnippet := false // !!! need snippet printer paramTagCount := 0 var tags []*ast.JSDocTag if jsDoc.AsJSDoc().Tags != nil { tags = jsDoc.AsJSDoc().Tags.Nodes } for _, tag := range tags { if ast.IsJSDocParameterTag(tag) && astnav.GetStartOfNode(tag, file, false /*includeJSDoc*/) < position && ast.IsIdentifier(tag.Name()) { paramTagCount++ } } paramIndex := -1 return core.MapNonNil(fun.Parameters(), func(param *ast.ParameterDeclarationNode) *lsproto.CompletionItem { paramIndex++ if paramIndex < paramTagCount { // This parameter is already annotated. return nil } if ast.IsIdentifier(param.Name()) { // Named parameter tabstopCounter := 1 paramName := param.Name().Text() displayText := getJSDocParamAnnotation( paramName, param.Initializer(), param.AsParameterDeclaration().DotDotDotToken, isJS, /*isObject*/ false, /*isSnippet*/ false, typeChecker, options, preferences, &tabstopCounter, ) var snippetText string if isSnippet { snippetText = getJSDocParamAnnotation( paramName, param.Initializer(), param.AsParameterDeclaration().DotDotDotToken, isJS, /*isObject*/ false, /*isSnippet*/ true, typeChecker, options, preferences, &tabstopCounter, ) } if tagNameOnly { // Remove `@` displayText = displayText[1:] if snippetText != "" { snippetText = snippetText[1:] } } return &lsproto.CompletionItem{ Label: displayText, Kind: ptrTo(lsproto.CompletionItemKindVariable), SortText: ptrTo(string(SortTextLocationPriority)), InsertText: strPtrTo(snippetText), InsertTextFormat: core.IfElse(isSnippet, ptrTo(lsproto.InsertTextFormatSnippet), nil), } } else if paramIndex == paramTagCount { // Destructuring parameter; do it positionally paramPath := fmt.Sprintf("param%d", paramIndex) displayTextResult := generateJSDocParamTagsForDestructuring( paramPath, param.Name(), param.Initializer(), param.AsParameterDeclaration().DotDotDotToken, isJS, /*isSnippet*/ false, typeChecker, options, preferences, ) var snippetText string if isSnippet { snippetTextResult := generateJSDocParamTagsForDestructuring( paramPath, param.Name(), param.Initializer(), param.AsParameterDeclaration().DotDotDotToken, isJS, /*isSnippet*/ true, typeChecker, options, preferences, ) snippetText = strings.Join(snippetTextResult, options.NewLine.GetNewLineCharacter()+"* ") } displayText := strings.Join(displayTextResult, options.NewLine.GetNewLineCharacter()+"* ") if tagNameOnly { // Remove `@` displayText = strings.TrimPrefix(displayText, "@") snippetText = strings.TrimPrefix(snippetText, "@") } return &lsproto.CompletionItem{ Label: displayText, Kind: ptrTo(lsproto.CompletionItemKindVariable), SortText: ptrTo(string(SortTextLocationPriority)), InsertText: strPtrTo(snippetText), InsertTextFormat: core.IfElse(isSnippet, ptrTo(lsproto.InsertTextFormatSnippet), nil), } } return nil }) } func getJSDocParamAnnotation( paramName string, initializer *ast.Expression, dotDotDotToken *ast.TokenNode, isJS bool, isObject bool, isSnippet bool, typeChecker *checker.Checker, options *core.CompilerOptions, preferences *UserPreferences, tabstopCounter *int, ) string { if isSnippet { debug.AssertIsDefined(tabstopCounter) } if initializer != nil { paramName = getJSDocParamNameWithInitializer(paramName, initializer) } if isSnippet { paramName = escapeSnippetText(paramName) } if isJS { t := "*" if isObject { debug.AssertNil(dotDotDotToken, `Cannot annotate a rest parameter with type 'object'.`) t = "object" } else { if initializer != nil { inferredType := typeChecker.GetTypeAtLocation(initializer.Parent) if inferredType.Flags()&(checker.TypeFlagsAny|checker.TypeFlagsVoid) == 0 { file := ast.GetSourceFileOfNode(initializer) quotePreference := getQuotePreference(file, preferences) builderFlags := core.IfElse( quotePreference == quotePreferenceSingle, nodebuilder.FlagsUseSingleQuotesForStringLiteralType, nodebuilder.FlagsNone, ) typeNode := typeChecker.TypeToTypeNode(inferredType, ast.FindAncestor(initializer, ast.IsFunctionLike), builderFlags) if typeNode != nil { emitContext := printer.NewEmitContext() // !!! snippet p p := printer.NewPrinter(printer.PrinterOptions{ RemoveComments: true, // !!! // Module: options.Module, // ModuleResolution: options.ModuleResolution, // Target: options.Target, }, printer.PrintHandlers{}, emitContext) emitContext.SetEmitFlags(typeNode, printer.EFSingleLine) t = p.Emit(typeNode, file) } } } if isSnippet && t == "*" { tabstop := *tabstopCounter *tabstopCounter++ t = fmt.Sprintf("${%d:%s}", tabstop, t) } } dotDotDot := core.IfElse(!isObject && dotDotDotToken != nil, "...", "") var description string if isSnippet { tabstop := *tabstopCounter *tabstopCounter++ description = fmt.Sprintf("${%d}", tabstop) } return fmt.Sprintf("@param {%s%s} %s %s", dotDotDot, t, paramName, description) } else { var description string if isSnippet { tabstop := *tabstopCounter *tabstopCounter++ description = fmt.Sprintf("${%d}", tabstop) } return fmt.Sprintf("@param %s %s", paramName, description) } } func getJSDocParamNameWithInitializer(paramName string, initializer *ast.Expression) string { initializerText := strings.TrimSpace(scanner.GetTextOfNode(initializer)) if strings.Contains(initializerText, "\n") || len(initializerText) > 80 { return fmt.Sprintf("[%s]", paramName) } return fmt.Sprintf("[%s=%s]", paramName, initializerText) } func generateJSDocParamTagsForDestructuring( path string, pattern *ast.BindingPatternNode, initializer *ast.Expression, dotDotDotToken *ast.TokenNode, isJS bool, isSnippet bool, typeChecker *checker.Checker, options *core.CompilerOptions, preferences *UserPreferences, ) []string { tabstopCounter := 1 if !isJS { return []string{getJSDocParamAnnotation( path, initializer, dotDotDotToken, isJS, /*isObject*/ false, isSnippet, typeChecker, options, preferences, &tabstopCounter, )} } return jsDocParamPatternWorker( path, pattern, initializer, dotDotDotToken, isJS, isSnippet, typeChecker, options, preferences, &tabstopCounter, ) } func jsDocParamPatternWorker( path string, pattern *ast.BindingPatternNode, initializer *ast.Expression, dotDotDotToken *ast.TokenNode, isJS bool, isSnippet bool, typeChecker *checker.Checker, options *core.CompilerOptions, preferences *UserPreferences, counter *int, ) []string { if ast.IsObjectBindingPattern(pattern) && dotDotDotToken == nil { childCounter := *counter rootParam := getJSDocParamAnnotation( path, initializer, dotDotDotToken, isJS, /*isObject*/ true, isSnippet, typeChecker, options, preferences, &childCounter, ) var childTags []string for _, element := range pattern.Elements() { elementTags := jsDocParamElementWorker( path, element, initializer, dotDotDotToken, isJS, isSnippet, typeChecker, options, preferences, &childCounter, ) if len(elementTags) == 0 { childTags = nil break } childTags = append(childTags, elementTags...) } if len(childTags) > 0 { *counter = childCounter return append([]string{rootParam}, childTags...) } } return []string{ getJSDocParamAnnotation( path, initializer, dotDotDotToken, isJS, /*isObject*/ false, isSnippet, typeChecker, options, preferences, counter, ), } } // Assumes binding element is inside object binding pattern. // We can't deeply annotate an array binding pattern. func jsDocParamElementWorker( path string, element *ast.BindingElementNode, initializer *ast.Expression, dotDotDotToken *ast.TokenNode, isJS bool, isSnippet bool, typeChecker *checker.Checker, options *core.CompilerOptions, preferences *UserPreferences, counter *int, ) []string { if ast.IsIdentifier(element.Name()) { // `{ b }` or `{ b: newB }` var propertyName string if element.PropertyName() != nil { propertyName, _ = ast.TryGetTextOfPropertyName(element.PropertyName()) } else { propertyName = element.Name().Text() } if propertyName == "" { return nil } paramName := fmt.Sprintf("%s.%s", path, propertyName) return []string{ getJSDocParamAnnotation( paramName, element.Initializer(), element.AsBindingElement().DotDotDotToken, isJS, /*isObject*/ false, isSnippet, typeChecker, options, preferences, counter, ), } } else if element.PropertyName() != nil { // `{ b: {...} }` or `{ b: [...] }` propertyName, _ := ast.TryGetTextOfPropertyName(element.PropertyName()) if propertyName == "" { return nil } return jsDocParamPatternWorker( fmt.Sprintf("%s.%s", path, propertyName), element.Name(), element.Initializer(), element.AsBindingElement().DotDotDotToken, isJS, isSnippet, typeChecker, options, preferences, counter, ) } return nil } func getJSDocParameterNameCompletions(tag *ast.JSDocParameterTag) []*lsproto.CompletionItem { if !ast.IsIdentifier(tag.Name()) { return nil } nameThusFar := tag.Name().Text() jsDoc := tag.Parent fn := jsDoc.Parent if !ast.IsFunctionLike(fn) { return nil } var tags []*ast.JSDocTag if jsDoc.AsJSDoc().Tags != nil { tags = jsDoc.AsJSDoc().Tags.Nodes } return core.MapNonNil(fn.Parameters(), func(param *ast.ParameterDeclarationNode) *lsproto.CompletionItem { if !ast.IsIdentifier(param.Name()) { return nil } name := param.Name().Text() if core.Some(tags, func(t *ast.JSDocTag) bool { return t != tag.AsNode() && ast.IsJSDocParameterTag(t) && ast.IsIdentifier(t.Name()) && t.Name().Text() == name }) || nameThusFar != "" && !strings.HasPrefix(name, nameThusFar) { return nil } return &lsproto.CompletionItem{ Label: name, Kind: ptrTo(lsproto.CompletionItemKindVariable), SortText: ptrTo(string(SortTextLocationPriority)), } }) }