package ls import ( "context" "errors" "fmt" "slices" "strings" "sync" "unicode" "unicode/utf8" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/format" "github.com/microsoft/typescript-go/internal/jsnum" "github.com/microsoft/typescript-go/internal/ls/autoimport" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/nodebuilder" "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" "github.com/microsoft/typescript-go/internal/tspath" ) var ErrNeedsAutoImports = errors.New("completion list needs auto imports") func (l *LanguageService) ProvideCompletion( ctx context.Context, documentURI lsproto.DocumentUri, LSPPosition lsproto.Position, context *lsproto.CompletionContext, ) (lsproto.CompletionResponse, error) { _, file := l.getProgramAndFile(documentURI) var triggerCharacter *string if context != nil { triggerCharacter = context.TriggerCharacter } ctx = format.WithFormatCodeSettings(ctx, l.FormatOptions(), l.FormatOptions().NewLineCharacter) position := int(l.converters.LineAndCharacterToPosition(file, LSPPosition)) completionListInternal, err := l.getCompletionsAtPosition( ctx, file, position, triggerCharacter, false, /*includeSymbols*/ ) if err != nil { return lsproto.CompletionItemsOrListOrNull{}, err } completionList := ensureItemData(file.FileName(), position, completionListInternal.toLSP()) return lsproto.CompletionItemsOrListOrNull{List: completionList}, nil } func (l *LanguageService) GetCompletionsAtPosition(ctx context.Context, file *ast.SourceFile, position int, triggerCharacter *string, includeSymbols bool) (*CompletionList, error) { return l.getCompletionsAtPosition(ctx, file, position, triggerCharacter, includeSymbols) } type CompletionItem struct { *lsproto.CompletionItem Symbol *ast.Symbol // non-nil for symbol completions when IncludeSymbols is set; nil otherwise } type CompletionList struct { IsIncomplete bool ItemDefaults *lsproto.CompletionItemDefaults ApplyKind *lsproto.CompletionItemApplyKinds Items []*CompletionItem } 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 { item.Data = &lsproto.CompletionItemData{ FileName: fileName, Position: int32(pos), Name: item.Label, } } } return list } // *completionDataData | *completionDataKeyword | *completionDataJSDocTagName | *completionDataJSDocTag | *completionDataJSDocParameterName type completionData = any type completionDataData struct { symbols []*ast.Symbol autoImports []*autoimport.FixAndExport 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[int]*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 []*CompletionItem isNewIdentifierLocation bool } type completionDataJSDocTagName struct{} type completionDataJSDocTag struct{} type completionDataJSDocParameterName struct { tag *ast.JSDocParameterOrPropertyTag } 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 symbolOriginInfoKindPromise symbolOriginInfoKindNullable symbolOriginInfoKindTypeOnlyAlias symbolOriginInfoKindObjectLiteralMethod symbolOriginInfoKindIgnore symbolOriginInfoKindComputedPropertyName ) type symbolOriginInfo struct { kind symbolOriginInfoKind isDefaultExport bool isFromPackageJson bool fileName string data any } func (origin *symbolOriginInfo) symbolName() string { switch origin.data.(type) { case *symbolOriginInfoComputedPropertyName: return origin.data.(*symbolOriginInfoComputedPropertyName).symbolName default: panic(fmt.Sprintf("symbolOriginInfo: unknown data type for symbolName(): %T", origin.data)) } } 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 *CompletionList) toLSP() *lsproto.CompletionList { if l == nil { return nil } items := make([]*lsproto.CompletionItem, 0, len(l.Items)) for _, entry := range l.Items { if entry != nil && entry.CompletionItem != nil { items = append(items, entry.CompletionItem) } } return &lsproto.CompletionList{ IsIncomplete: l.IsIncomplete, ItemDefaults: l.ItemDefaults, ApplyKind: l.ApplyKind, Items: items, } } func (l *LanguageService) getCompletionsAtPosition( ctx context.Context, file *ast.SourceFile, position int, triggerCharacter *string, includeSymbols bool, ) (*CompletionList, error) { _, previousToken := getRelevantTokens(position, file) if triggerCharacter != nil && !IsInString(file, position, previousToken) && !isValidTrigger(file, *triggerCharacter, previousToken, position) { return nil, nil } if triggerCharacter != nil && *triggerCharacter == " " { // `isValidTrigger` ensures we are at `import |` if l.UserPreferences().IncludeCompletionsForImportStatements.IsTrue() { return &CompletionList{IsIncomplete: true}, nil } return nil, nil } if jsDocSnippetCompletion := l.getJSDocSnippetCompletion(ctx, file, position); jsDocSnippetCompletion != nil { return jsDocSnippetCompletion, nil } compilerOptions := l.GetProgram().Options() // !!! see if incomplete completion list and continue or clean checker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file) defer done() stringCompletions := l.getStringLiteralCompletions( ctx, file, position, previousToken, checker, compilerOptions, includeSymbols, ) if stringCompletions != nil { return stringCompletions, nil } if previousToken != nil && (previousToken.Kind == ast.KindBreakKeyword || previousToken.Kind == ast.KindContinueKeyword || previousToken.Kind == ast.KindIdentifier) && ast.IsBreakOrContinueStatement(previousToken.Parent) { return l.getLabelCompletionsAtPosition( ctx, previousToken.Parent, file, position, l.getOptionalReplacementSpan(previousToken, file), ), nil } preferences := l.UserPreferences() data, err := l.getCompletionData(ctx, checker, file, position, preferences, false /*forItemResolve*/) if err != nil { return nil, err } if data == nil { return nil, nil } switch data := data.(type) { case *completionDataData: optionalReplacementSpan := l.getOptionalReplacementSpan(data.location, file) response, err := l.completionInfoFromData( ctx, checker, file, compilerOptions, data, position, optionalReplacementSpan, includeSymbols, ) if err != nil { return nil, err } return response, nil case *completionDataKeyword: optionalReplacementSpan := l.getOptionalReplacementSpan(previousToken, file) return l.specificKeywordCompletionInfo( ctx, position, file, data.keywordCompletions, data.isNewIdentifierLocation, optionalReplacementSpan, ), nil 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( ctx, file, position, checker, compilerOptions, preferences, /*tagNameOnly*/ true, )...) return l.jsDocCompletionInfo(ctx, position, file, items), nil case *completionDataJSDocTag: // If the current position is a jsDoc tag, only tags should be provided for completion items := getJSDocTagCompletions() items = append(items, getJSDocParameterCompletions( ctx, file, position, checker, compilerOptions, preferences, /*tagNameOnly*/ false, )...) return l.jsDocCompletionInfo(ctx, position, file, items), nil case *completionDataJSDocParameterName: return l.jsDocCompletionInfo(ctx, position, file, getJSDocParameterNameCompletions(data.tag)), nil 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 lsutil.UserPreferences, forItemResolve bool, ) (completionData, error) { 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 position > 0 && file.Text()[position-1] == '@' { // The current position is next to the '@' sign, when no tag name being provided yet. // Provide a full list of tag names return &completionDataJSDocTagName{}, nil } 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{}, nil } } } // 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{}, nil } 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(), }, nil } } } 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, 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: []*CompletionItem{{ CompletionItem: &lsproto.CompletionItem{ Label: scanner.TokenToString(importStatementCompletionInfo.keywordCompletion), Kind: new(lsproto.CompletionItemKindKeyword), SortText: new(string(SortTextGlobalsOrKeywords)), }, }}, isNewIdentifierLocation: importStatementCompletionInfo.isNewIdentifierLocation, }, nil } 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), nil } return nil, 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, 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, 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 && astnav.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 var autoImports []*autoimport.FixAndExport // Keys are indexes of `symbols`. symbolToOriginInfoMap := map[int]*symbolOriginInfo{} symbolToSortTextMap := map[ast.SymbolId]SortText{} var seenPropertySymbols collections.Set[ast.SymbolId] isTypeOnlyLocation := insideJSDocTagTypeExpression || insideJsDocImportTag || importStatementCompletion != nil && location.Parent != 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[len(symbols)-1] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindPromise, insertQuestionDot)} } else if insertQuestionDot { symbolToOriginInfoMap[len(symbols)-1] = &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[len(symbols)-1] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMember, insertQuestionDot)} } else { // !!! auto-import symbol } } 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, error) { typeLiteralNode := tryGetTypeLiteralNode(contextToken) if typeLiteralNode == nil { return globalsSearchContinue, nil } 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, nil } 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, nil } // Aggregates relevant symbols for completion in object literals and object binding patterns. // Relevant symbols are stored in the captured 'symbols' variable. tryGetObjectLikeCompletionSymbols := func() (globalsSearch, error) { if contextToken != nil && contextToken.Kind == ast.KindDotDotDotToken { return globalsSearchContinue, nil } objectLikeContainer := tryGetObjectLikeCompletionContainer(contextToken, position, file) if objectLikeContainer == nil { return globalsSearchContinue, nil } // 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, nil } return globalsSearchContinue, nil } completionsType := typeChecker.GetContextualType(objectLikeContainer, checker.ContextFlagsIgnoreNodeInferences) 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) existingMembers = objectLikeContainer.Properties() if len(typeMembers) == 0 { // Edge case: If NumberIndexType exists if numberIndexType == nil { return globalsSearchContinue, nil } } } 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, nil } typeMembers = core.Filter( typeChecker.GetPropertiesOfType(typeForObject), func(propertySymbol *ast.Symbol) bool { return typeChecker.IsPropertyAccessible( objectLikeContainer, false, /*isSuper*/ false, /*isWrite*/ typeForObject, propertySymbol, ) }, ) existingMembers = objectLikeContainer.Elements() } } 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, nil } shouldOfferImportCompletions := func() bool { if tspath.IsDynamicFileName(file.FileName()) { return false } // 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.IsFalse() { return false } // Always using ES modules in 6.0+ return true } // Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` collectAutoImports := func() error { // `completionItem/resolve` for auto-import completions should be resolved via the completion item data, // so we don't need to collect auto-import entries again. if forItemResolve { return nil } if !shouldOfferImportCompletions() { return nil } // import { type | -> token text should be blank var lowerCaseTokenText string usagePosition := l.createLspPosition(position, file) if previousToken != nil && ast.IsIdentifier(previousToken) { usagePosition = l.createLspPosition(scanner.GetTokenPosOfNode(previousToken, file, false /*includeJSDoc*/), file) if !(previousToken == contextToken && importStatementCompletion != nil) { lowerCaseTokenText = strings.ToLower(previousToken.Text()) } } view, err := l.getPreparedAutoImportView(file) if err != nil { return err } if view == nil { return nil } autoImports = view.GetCompletions(ctx, lowerCaseTokenText, usagePosition, isRightOfOpenTag, isTypeOnlyLocation) return nil } tryGetImportCompletionSymbols := func() (globalsSearch, error) { if importStatementCompletion == nil { return globalsSearchContinue, nil } isNewIdentifierLocation = true if err := collectAutoImports(); err != nil { return globalsSearchFail, err } return globalsSearchSuccess, nil } // 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, error) { if contextToken == nil { return globalsSearchContinue, nil } // `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, nil } // 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, nil } return globalsSearchContinue, nil } moduleSpecifierSymbol := typeChecker.GetSymbolAtLocation(moduleSpecifier) if moduleSpecifierSymbol == nil { isNewIdentifierLocation = true return globalsSearchFail, nil } 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, nil } // import { x } from "foo" with { | } tryGetImportAttributesCompletionSymbols := func() (globalsSearch, error) { if contextToken == nil { return globalsSearchContinue, nil } var importAttributes *ast.Node switch contextToken.Kind { case ast.KindOpenBraceToken, ast.KindCommaToken: importAttributes = contextToken.Parent case ast.KindColonToken: importAttributes = contextToken.Parent.Parent } if importAttributes == nil || !ast.IsImportAttributes(importAttributes) { return globalsSearchContinue, nil } var elements []*ast.Node if importAttributes.AsImportAttributes().Attributes != nil { elements = importAttributes.AsImportAttributes().Attributes.Nodes } attributeNames := core.Map(elements, func(el *ast.Node) string { return el.AsImportAttribute().Name().Text() }) existing := collections.NewSetFromItems(attributeNames...) 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, nil } // 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, error) { if contextToken == nil { return globalsSearchContinue, nil } 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, nil } 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, nil } tryGetConstructorCompletion := func() (globalsSearch, error) { if tryGetConstructorLikeCompletionContainer(contextToken) == nil { return globalsSearchContinue, nil } // no members, only keywords completionKind = CompletionKindNone // Declaring new property/method/accessor isNewIdentifierLocation = true // Has keywords for constructor parameter keywordFilters = KeywordCompletionFiltersConstructorParameterKeywords return globalsSearchSuccess, nil } // Aggregates relevant symbols for completion in class declaration // Relevant symbols are stored in the captured 'symbols' variable. tryGetClassLikeCompletionSymbols := func() (globalsSearch, error) { decl := tryGetObjectTypeDeclarationCompletionContainer(file, contextToken, location, position) if decl == nil { return globalsSearchContinue, nil } // 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, nil } 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 index, symbol := range symbols { declaration := symbol.ValueDeclaration if declaration != nil && ast.IsClassElement(declaration) && declaration.Name() != nil && ast.IsComputedPropertyName(declaration.Name()) { origin := &symbolOriginInfo{ kind: symbolOriginInfoKindComputedPropertyName, data: &symbolOriginInfoComputedPropertyName{symbolName: typeChecker.SymbolToString(symbol)}, } symbolToOriginInfoMap[index] = origin } } } return globalsSearchSuccess, nil } tryGetJsxCompletionSymbols := func() (globalsSearch, error) { jsxContainer := tryGetContainingJsxElement(contextToken, file) if jsxContainer == nil { return globalsSearchContinue, nil } // Cursor is inside a JSX self-closing element or opening element. attrsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsNone) if attrsType == nil { return globalsSearchContinue, nil } completionsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsIgnoreNodeInferences) 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, nil } getGlobalCompletions := func() (globalsSearch, error) { 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 index, 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[index] = 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[len(symbols)-1] = &symbolOriginInfo{kind: symbolOriginInfoKindThisType} symbolToSortTextMap[symbolId] = SortTextSuggestedClassMembers } } } if err := collectAutoImports(); err != nil { return globalsSearchFail, err } if isTypeOnlyLocation { if contextToken != nil && ast.IsAssertionExpression(contextToken.Parent) { keywordFilters = KeywordCompletionFiltersTypeAssertionKeywords } else { keywordFilters = KeywordCompletionFiltersTypeKeywords } } return globalsSearchSuccess, nil } tryGetGlobalSymbols := func() (bool, error) { var result globalsSearch var err error globalSearchFuncs := []func() (globalsSearch, error){ tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols, tryGetObjectLikeCompletionSymbols, tryGetImportCompletionSymbols, tryGetImportOrExportClauseCompletionSymbols, tryGetImportAttributesCompletionSymbols, tryGetLocalNamedExportCompletionSymbols, tryGetConstructorCompletion, tryGetClassLikeCompletionSymbols, tryGetJsxCompletionSymbols, getGlobalCompletions, } for _, globalSearchFunc := range globalSearchFuncs { result, err = globalSearchFunc() if err != nil { return false, err } if result != globalsSearchContinue { break } } return result == globalsSearchSuccess, nil } if isRightOfDot || isRightOfQuestionDot { getTypeScriptMemberSymbols() } else if isRightOfOpenTag { symbols = typeChecker.GetJsxIntrinsicTagNamesAt(location) core.CheckEachDefined(symbols, "GetJsxIntrinsicTagNamesAt() should all be defined") if _, err := tryGetGlobalSymbols(); err != nil { return nil, err } 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 ok, err := tryGetGlobalSymbols(); !ok { if err != nil { return nil, err } if keywordFilters != KeywordCompletionFiltersNone { return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation), nil } return nil, nil } } var contextualTypeOrConstraint *checker.Type if previousToken != nil { contextualTypeOrConstraint = getContextualType(previousToken, position, file, typeChecker) if contextualTypeOrConstraint == nil { contextualTypeOrConstraint = getConstraintOfTypeArgumentProperty(previousToken, 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 contextualTypeOrConstraint != nil && contextualTypeOrConstraint.IsUnion() { types = contextualTypeOrConstraint.Types() } else if contextualTypeOrConstraint != nil { types = []*checker.Type{contextualTypeOrConstraint} } 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 && contextualTypeOrConstraint != nil { recommendedCompletion = getRecommendedCompletion(previousToken, contextualTypeOrConstraint, typeChecker) } if defaultCommitCharacters == nil { defaultCommitCharacters = getDefaultCommitCharacters(isNewIdentifierLocation) } return &completionDataData{ symbols: symbols, autoImports: autoImports, 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, }, nil } 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, position int, optionalReplacementSpan *lsproto.Range, includeSymbols bool, ) (*CompletionList, error) { keywordFilters := data.keywordFilters isNewIdentifierLocation := data.isNewIdentifierLocation contextToken := data.contextToken literals := data.literals preferences := l.UserPreferences() // Verify if the file is JSX language variant if file.LanguageVariant == core.LanguageVariantJSX { list := l.getJsxClosingTagCompletion(ctx, data.location, file, position) if list != nil { return list, nil } } // 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, nil } uniqueNames, sortedEntries := l.getCompletionEntriesFromSymbols( ctx, typeChecker, data, nil, /*replacementToken*/ position, file, compilerOptions, includeSymbols, ) 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 = append(sortedEntries, keywordEntry) } } } for _, keywordEntry := range getContextualKeywords(file, contextToken, position) { if !uniqueNames.Has(keywordEntry.Label) { uniqueNames.Add(keywordEntry.Label) sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: keywordEntry}) } } for _, literal := range literals { literalEntry := createCompletionItemForLiteral(file, preferences, literal) uniqueNames.Add(literalEntry.Label) sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: literalEntry}) } if !isChecked { sortedEntries = l.getJSCompletionEntries( ctx, file, position, &uniqueNames, sortedEntries, ) } if contextToken != nil && !data.isRightOfOpenTag && !data.isRightOfDotOrQuestionDot { if caseBlock := ast.FindAncestorKind(contextToken, ast.KindCaseBlock); caseBlock != nil { casesItem, err := l.getExhaustiveCaseSnippets( ctx, caseBlock.AsCaseBlock(), file, position, compilerOptions, l.program, typeChecker, ) if err != nil { return nil, err } if casesItem != nil { sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: casesItem}) } } } itemDefaults := l.setItemDefaults( ctx, position, file, sortedEntries, &data.defaultCommitCharacters, optionalReplacementSpan, ) return &CompletionList{ IsIncomplete: data.hasUnresolvedAutoImports, ItemDefaults: itemDefaults, Items: sortedEntries, }, nil } func (l *LanguageService) getCompletionEntriesFromSymbols( ctx context.Context, typeChecker *checker.Checker, data *completionDataData, replacementToken *ast.Node, position int, file *ast.SourceFile, compilerOptions *core.CompilerOptions, includeSymbols bool, ) (uniqueNames collections.Set[string], sortedEntries []*CompletionItem) { closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location) useSemicolons := lsutil.ProbablyUsesSemicolons(file) isMemberCompletion := isMemberCompletionKind(data.completionKind) sortedEntries = slices.Grow(sortedEntries, len(data.symbols)+len(data.autoImports)) // 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 index, symbol := range data.symbols { origin := data.symbolToOriginInfoMap[index] 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, 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 var sym *ast.Symbol if includeSymbols { sym = symbol } sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: entry, Symbol: sym}) } for _, autoImport := range data.autoImports { // !!! check for type-only in JS // !!! deprecation if data.importStatementCompletion != nil { // !!! continue } // Non-contextual keywords (e.g., `function`, `class`, `const`) cannot be used as identifiers, // so auto-imports with these names should not shadow keyword completions. if token := scanner.StringToToken(autoImport.Fix.Name); token != ast.KindUnknown && ast.IsNonContextualKeyword(token) { continue } if !autoImport.Export.IsUnresolvedAlias() { if data.isTypeOnlyLocation { if autoImport.Export.Flags&ast.SymbolFlagsType == 0 && autoImport.Export.Flags&ast.SymbolFlagsModule == 0 { continue } } else if autoImport.Export.Flags&ast.SymbolFlagsValue == 0 { continue } } entry := l.createLSPCompletionItem( ctx, autoImport.Fix.Name, "", "", SortTextAutoImportSuggestions, autoImport.Export.ScriptElementKind, autoImport.Export.ScriptElementKindModifiers, nil, nil, &lsproto.CompletionItemLabelDetails{ Description: new(autoImport.Fix.ModuleSpecifier), }, file, position, false, /*isMemberCompletion*/ false, /*isSnippet*/ true, /*hasAction*/ false, /*preselect*/ autoImport.Fix.ModuleSpecifier, autoImport.Fix.AutoImportFix, nil, /*detail*/ ) if isShadowed, _ := uniques[autoImport.Fix.Name]; !isShadowed { uniques[autoImport.Fix.Name] = false sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: entry}) } } uniqueSet := collections.NewSetWithSizeHint[string](len(uniques)) for name := range uniques { uniqueSet.Add(name) } return *uniqueSet, sortedEntries } func completionNameForLiteral( file *ast.SourceFile, preferences lsutil.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 lsutil.UserPreferences, literal literalValue, ) *lsproto.CompletionItem { return &lsproto.CompletionItem{ Label: completionNameForLiteral(file, preferences, literal), Kind: new(lsproto.CompletionItemKindConstant), SortText: new(string(SortTextLocationPriority)), CommitCharacters: new([]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, 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 preferences := l.UserPreferences() insertQuestionDot := originIsNullableMember(origin) useBraces := originIsSymbolMember(origin) || needsConvertPropertyAccess if originIsThisTypeNode(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.QuestionDotToken() != nil { insertText = "?." + insertText } dot := astnav.FindChildOfKind(data.propertyAccessToConvert, ast.KindDotToken, file) if dot == nil { dot = astnav.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 = new(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 = new(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 = new(l.createLspRangeFromBounds( astnav.GetStartOfNode(wrapNode, file, false /*includeJSDoc*/), data.propertyAccessToConvert.End(), file, )) } 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(ctx) { name = name + *origin.asObjectLiteralMethod().labelDetails.Detail labelDetails = nil } source = string(completionSourceObjectLiteralMethodSnippet) sortText = sortBelow(sortText) } if data.isJsxIdentifierExpected && !data.isRightOfOpenTag && clientSupportsItemSnippet(ctx) && preferences.JsxAttributeCompletionStyle != lsutil.JsxAttributeCompletionStyleNone && !(data.location.Parent != nil && ast.IsJsxAttribute(data.location.Parent) && data.location.Parent.Initializer() != nil) { useBraces := preferences.JsxAttributeCompletionStyle == lsutil.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 == lsutil.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 } } 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 || lsutil.IsNonContextualKeyword(possibleToken)) { insertText = fmt.Sprintf("%s as %s_", name, name) } } } // Commit characters elementKind := lsutil.GetSymbolKind(typeChecker, symbol, data.location) var commitCharacters *[]string if clientSupportsItemCommitCharacters(ctx) { if elementKind == lsutil.ScriptElementKindWarning || elementKind == lsutil.ScriptElementKindString { commitCharacters = &[]string{} } else if !clientSupportsDefaultCommitCharacters(ctx) { commitCharacters = new(data.defaultCommitCharacters) } // Otherwise use the completion list default. } preselect := isRecommendedCompletionMatch(symbol, data.recommendedCompletion, typeChecker) kindModifiers := lsutil.GetSymbolModifiers(typeChecker, symbol) return l.createLSPCompletionItem( ctx, name, insertText, filterText, sortText, elementKind, kindModifiers, replacementSpan, commitCharacters, labelDetails, file, position, isMemberCompletion, isSnippet, hasAction, preselect, source, nil, /*autoImportFix*/ nil, /*detail*/ ) } 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 after, ok := strings.CutPrefix(label, "#"); ok { if insertText != "" { if after, ok := strings.CutPrefix(insertText, "this.#"); ok { if wordStart == '#' { // `method() { this.#| }` // `method() { #| }` return "" } else { // `method() { this.| }` // `method() { | }` return after } } } else { if wordStart == '#' { // `method() { this.#| }` return "" } else { // `method() { this.| }` // `method() { | }` return after } } } // 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 boolToPtr(v bool) *bool { if v { return new(true) } return nil } func getLineOfPosition(file *ast.SourceFile, pos int) int { line := scanner.GetECMALineOfPosition(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.SkipAlias(symbol, typeChecker).CombinedLocalAndExportSymbolFlags() 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.IsParameterDeclaration(closestSymbolDeclaration) && ast.IsParameterDeclaration(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 && symbol != symbolOrigin && data.symbolToSortTextMap[ast.GetSymbolId(symbol)] == SortTextGlobalsOrKeywords && symbol.Parent != nil && checker.IsExternalModuleSymbol(symbol.Parent) { return false } allFlags = allFlags | symbolOrigin.CombinedLocalAndExportSymbolFlags() if symbol.Flags&ast.SymbolFlagsAlias != 0 { allFlags = allFlags | typeChecker.GetSymbolFlags(symbol) } // 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 originIsComputedPropertyName(origin) } 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 originIsThisTypeNode(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 originIsThisTypeNode(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 ast.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 case "*": return isPotentiallyValidJSDocSnippetCompletionPosition(file, position) 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) } // getContextualTypeForConditionalExpression handles completion within a conditional expression // (ternary operator) by using the parent expression to find the contextual type. func getContextualTypeForConditionalExpression(conditionalExpr *ast.Node, position int, file *ast.SourceFile, typeChecker *checker.Checker) *checker.Type { argInfo := getArgumentInfoForCompletions(conditionalExpr, position, file, typeChecker) if argInfo != nil { return typeChecker.GetContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex) } // Fall through to regular contextual type logic if not in an argument contextualType := typeChecker.GetContextualType(conditionalExpr, checker.ContextFlagsIgnoreNodeInferences) if contextualType != nil { return contextualType } return typeChecker.GetContextualType(conditionalExpr, checker.ContextFlagsNone) } 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 case ast.KindOpenBracketToken: // When completing after `[` in an array literal (e.g., `[/*here*/]`), // we should provide contextual type for the first element if ast.IsArrayLiteralExpression(parent) { contextualArrayType := typeChecker.GetContextualType(parent, checker.ContextFlagsNone) if contextualArrayType != nil { // Get the type for the first element (index 0) return typeChecker.GetContextualTypeForArrayLiteralAtPosition(contextualArrayType, parent, position) } } return nil case ast.KindCloseBracketToken: // When completing after `]` (e.g., `[x]/*here*/`), we should not provide a contextual type // for the closing bracket token itself. Without this case, CloseBracketToken would fall through // to the default case, and if the parent is an array literal, GetContextualType would try to // find the token's index in the array elements (returning -1), leading to an out-of-bounds panic // in getContextualTypeForElementExpression. return nil case ast.KindQuestionToken: // When completing after `?` in a ternary conditional (e.g., `foo(a ? /*here*/)`), // we need to look at the parent conditional expression to find the contextual type. if ast.IsConditionalExpression(parent) { return getContextualTypeForConditionalExpression(parent, position, file, typeChecker) } return nil case ast.KindColonToken: // When completing after `:` in a ternary conditional (e.g., `foo(a ? b : /*here*/)`), // we need to look at the parent conditional expression to find the contextual type. // Only handle this if parent is ConditionalExpression, otherwise fall through to default // (colons are used in other contexts like object literals, type annotations, etc.) if ast.IsConditionalExpression(parent) { return getContextualTypeForConditionalExpression(parent, position, file, typeChecker) } case ast.KindCommaToken: // When completing after `,` in an array literal (e.g., `[x, /*here*/]`), // we should provide contextual type for the element after the comma. if ast.IsArrayLiteralExpression(parent) { contextualArrayType := typeChecker.GetContextualType(parent, checker.ContextFlagsNone) if contextualArrayType != nil { return typeChecker.GetContextualTypeForArrayLiteralAtPosition(contextualArrayType, parent, position) } return nil } } // Default case: see if we're in an argument position. 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.ContextFlagsIgnoreNodeInferences) if contextualType != nil { return contextualType } return typeChecker.GetContextualType(previousToken, checker.ContextFlagsNone) } } func getSwitchedType(caseClause *ast.CaseOrDefaultClauseNode, 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 } } // We disregard boolean literals for completion purposes. 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.IsParameterDeclaration(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.AsTypeParameterDeclaration().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 new(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 new(l.createLspRangeFromBounds(nodeStart+1, replacementEnd, file)) } func quotePropertyName(file *ast.SourceFile, preferences lsutil.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 var identifier strings.Builder 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.WriteRune('_') } identifier.WriteRune(ch) needsUnderscore = false } else { needsUnderscore = true } } if needsUnderscore { identifier.WriteRune('_') } // Default to "_" if the provided text was empty id := identifier.String() if id == "" { return "_" } return id } // Copied from vscode TS extension. func getCompletionsSymbolKind(kind lsutil.ScriptElementKind) lsproto.CompletionItemKind { switch kind { case lsutil.ScriptElementKindPrimitiveType, lsutil.ScriptElementKindKeyword: return lsproto.CompletionItemKindKeyword case lsutil.ScriptElementKindConstElement, lsutil.ScriptElementKindLetElement, lsutil.ScriptElementKindVariableElement, lsutil.ScriptElementKindLocalVariableElement, lsutil.ScriptElementKindAlias, lsutil.ScriptElementKindParameterElement: return lsproto.CompletionItemKindVariable case lsutil.ScriptElementKindMemberVariableElement, lsutil.ScriptElementKindMemberGetAccessorElement, lsutil.ScriptElementKindMemberSetAccessorElement: return lsproto.CompletionItemKindField case lsutil.ScriptElementKindFunctionElement, lsutil.ScriptElementKindLocalFunctionElement: return lsproto.CompletionItemKindFunction case lsutil.ScriptElementKindMemberFunctionElement, lsutil.ScriptElementKindConstructSignatureElement, lsutil.ScriptElementKindCallSignatureElement, lsutil.ScriptElementKindIndexSignatureElement: return lsproto.CompletionItemKindMethod case lsutil.ScriptElementKindEnumElement: return lsproto.CompletionItemKindEnum case lsutil.ScriptElementKindEnumMemberElement: return lsproto.CompletionItemKindEnumMember case lsutil.ScriptElementKindModuleElement, lsutil.ScriptElementKindExternalModuleName: return lsproto.CompletionItemKindModule case lsutil.ScriptElementKindClassElement, lsutil.ScriptElementKindTypeElement: return lsproto.CompletionItemKindClass case lsutil.ScriptElementKindInterfaceElement: return lsproto.CompletionItemKindInterface case lsutil.ScriptElementKindWarning: return lsproto.CompletionItemKindText case lsutil.ScriptElementKindScriptElement: return lsproto.CompletionItemKindFile case lsutil.ScriptElementKindDirectory: return lsproto.CompletionItemKindFolder case lsutil.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. func CompareCompletionEntries(a, b *lsproto.CompletionItem) int { compareStrings := stringutil.CompareStringsCaseInsensitiveThenSensitive result := compareStrings(*a.SortText, *b.SortText) if result == stringutil.ComparisonEqual { result = compareStrings(a.Label, b.Label) } return result } 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: new(lsproto.CompletionItemKindKeyword), SortText: new(string(SortTextGlobalsOrKeywords)), }) } return result }) ) func cloneItems(items []*lsproto.CompletionItem) []*CompletionItem { if items == nil { return nil } entries := make([]*CompletionItem, len(items)) for i, item := range items { itemClone := *item entries[i] = &CompletionItem{CompletionItem: &itemClone} } return entries } func getKeywordCompletions(keywordFilter KeywordCompletionFilters, filterOutTsOnlyKeywords bool) []*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.GetECMALineOfPosition(file, contextToken.End()) currentLine := scanner.GetECMALineOfPosition(file, position) if (ast.IsImportDeclaration(parent) || ast.IsExportDeclaration(parent) && parent.ModuleSpecifier() != nil) && contextToken == parent.ModuleSpecifier() && tokenLine == currentLine { entries = append(entries, &lsproto.CompletionItem{ Label: scanner.TokenToString(ast.KindAssertKeyword), Kind: new(lsproto.CompletionItemKindKeyword), SortText: new(string(SortTextGlobalsOrKeywords)), }) } } return entries } func (l *LanguageService) getJSCompletionEntries( ctx context.Context, file *ast.SourceFile, position int, uniqueNames *collections.Set[string], sortedEntries []*CompletionItem, ) []*CompletionItem { nameTable := file.GetNameTable() 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 = append(sortedEntries, &CompletionItem{ CompletionItem: &lsproto.CompletionItem{ Label: name, Kind: new(lsproto.CompletionItemKindText), SortText: new(string(SortTextJavascriptIdentifiers)), CommitCharacters: new([]string{}), }, }) } } 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 new(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.TypeLiteralNodeNode { 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) { constraint := typeChecker.GetTypeArgumentConstraint(node) if constraint != nil { return constraint } } t := getConstraintOfTypeArgumentProperty(node.Parent, typeChecker) if t == nil { return nil } switch node.Kind { case ast.KindPropertySignature: // Try to get the reparsed node first - we may be in JSDoc. reparsed := ast.GetReparsedNodeForNode(node) if symbol := reparsed.Symbol(); symbol != nil { return typeChecker.GetTypeOfPropertyOfContextualType(t, symbol.Name) } // In some cases, we won't have a corresponding symbol // (e.g. JSDoc types that never get re-attached) so we'll use // the name as declared by the property as a best-effort. if name, ok := ast.TryGetTextOfPropertyName(reparsed.Name()); ok { return typeChecker.GetTypeOfPropertyOfContextualType(t, name) } return nil case ast.KindColonToken: if node.Parent.Kind == ast.KindPropertySignature { // The cursor is at a property value location like `Foo<{ x: | }`. // `t` already refers to the appropriate property type. return t } case ast.KindIntersectionType, ast.KindTypeLiteral, ast.KindUnionType: return t case ast.KindOpenBracketToken: return typeChecker.GetElementTypeOfArrayType(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.PropertyName() != nil { // include only identifiers in completion list if member.PropertyName().Kind == ast.KindIdentifier { existingName = member.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 && t.Flags()&checker.TypeFlagsStructuredType != 0 { 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.IsParameterDeclaration(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.StatementList() if stmtList != nil && len(stmtList.Nodes) > 0 && ast.IsObjectTypeDeclaration(stmtList.Nodes[len(stmtList.Nodes)-1]) { cls := stmtList.Nodes[len(stmtList.Nodes)-1] if astnav.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.KindJsxNamespacedName, 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 && ast.IsJsxNamespacedName(parent) && parent.Parent != nil && (parent.Parent.Kind == ast.KindJsxSelfClosingElement || parent.Parent.Kind == ast.KindJsxOpeningElement) { return parent.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( ctx context.Context, position int, file *ast.SourceFile, items []*CompletionItem, defaultCommitCharacters *[]string, optionalReplacementSpan *lsproto.Range, ) *lsproto.CompletionItemDefaults { var itemDefaults *lsproto.CompletionItemDefaults if defaultCommitCharacters != nil { supportsItemCommitCharacters := clientSupportsItemCommitCharacters(ctx) if clientSupportsDefaultCommitCharacters(ctx) && 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(ctx) { 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(ctx) { 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( ctx context.Context, position int, file *ast.SourceFile, items []*CompletionItem, isNewIdentifierLocation bool, optionalReplacementSpan *lsproto.Range, ) *CompletionList { defaultCommitCharacters := getDefaultCommitCharacters(isNewIdentifierLocation) itemDefaults := l.setItemDefaults( ctx, position, file, items, &defaultCommitCharacters, optionalReplacementSpan, ) return &CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, } } func (l *LanguageService) getJsxClosingTagCompletion( ctx context.Context, location *ast.Node, file *ast.SourceFile, position int, ) *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 := astnav.FindChildOfKind(jsxClosingElement, ast.KindGreaterThanToken, file) != nil tagName := jsxClosingElement.Parent.AsJsxElement().OpeningElement.TagName() closingTag := scanner.GetTextOfNode(tagName) fullClosingTag := closingTag + core.IfElse(hasClosingAngleBracket, "", ">") optionalReplacementSpan := new(l.createLspRangeFromNode(jsxClosingElement.TagName(), file)) defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) lspItem := l.createLSPCompletionItem( ctx, fullClosingTag, /*name*/ "", /*insertText*/ "", /*filterText*/ SortTextLocationPriority, lsutil.ScriptElementKindClassElement, lsutil.ScriptElementKindModifierNone, /*kindModifiers*/ nil, /*replacementSpan*/ nil, /*commitCharacters*/ nil, /*labelDetails*/ file, position, true, /*isMemberCompletion*/ false, /*isSnippet*/ false, /*hasAction*/ false, /*preselect*/ "", /*source*/ nil, /*autoImportEntryData*/ // !!! jsx autoimports nil, /*detail*/ ) item := &CompletionItem{ CompletionItem: lspItem, } items := []*CompletionItem{item} itemDefaults := l.setItemDefaults( ctx, position, file, items, &defaultCommitCharacters, optionalReplacementSpan, ) return &CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, } } func (l *LanguageService) createLSPCompletionItem( ctx context.Context, name string, insertText string, filterText string, sortText SortText, elementKind lsutil.ScriptElementKind, kindModifiers lsutil.ScriptElementKindModifier, replacementSpan *lsproto.Range, commitCharacters *[]string, labelDetails *lsproto.CompletionItemLabelDetails, file *ast.SourceFile, position int, isMemberCompletion bool, isSnippet bool, hasAction bool, preselect bool, source string, autoImportFix *lsproto.AutoImportFix, detail *string, ) *lsproto.CompletionItem { kind := getCompletionsSymbolKind(elementKind) data := &lsproto.CompletionItemData{ FileName: file.FileName(), Position: int32(position), Source: source, Name: name, AutoImport: autoImportFix, } // 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 // Copied from vscode ts extension: `MyCompletionItem.constructor`. if kindModifiers&lsutil.ScriptElementKindModifierOptional != 0 { if insertText == "" { insertText = name } if filterText == "" { filterText = name } name = name + "?" } if kindModifiers&lsutil.ScriptElementKindModifierDeprecated != 0 { tags = &[]lsproto.CompletionItemTag{lsproto.CompletionItemTagDeprecated} } if hasAction && source != "" { // !!! adjust label like vscode does } // Client assumes plain text by default. var insertTextFormat *lsproto.InsertTextFormat if isSnippet { insertTextFormat = new(lsproto.InsertTextFormatSnippet) } return &lsproto.CompletionItem{ Label: name, LabelDetails: labelDetails, Kind: &kind, Tags: tags, Detail: detail, Preselect: boolToPtr(preselect), SortText: new(string(sortText)), FilterText: strPtrTo(filterText), InsertText: strPtrTo(insertText), InsertTextFormat: insertTextFormat, TextEdit: textEdit, CommitCharacters: commitCharacters, Data: data, } } func (l *LanguageService) getLabelCompletionsAtPosition( ctx context.Context, node *ast.BreakOrContinueStatement, file *ast.SourceFile, position int, optionalReplacementSpan *lsproto.Range, ) *CompletionList { items := l.getLabelStatementCompletions(ctx, node, file, position) if len(items) == 0 { return nil } defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) itemDefaults := l.setItemDefaults( ctx, position, file, items, &defaultCommitCharacters, optionalReplacementSpan, ) return &CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, } } func (l *LanguageService) getLabelStatementCompletions( ctx context.Context, node *ast.BreakOrContinueStatement, file *ast.SourceFile, position int, ) []*CompletionItem { var uniques collections.Set[string] var items []*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) lspItem := l.createLSPCompletionItem( ctx, name, "", /*insertText*/ "", /*filterText*/ SortTextLocationPriority, lsutil.ScriptElementKindLabel, lsutil.ScriptElementKindModifierNone, /*kindModifiers*/ nil, /*replacementSpan*/ nil, /*commitCharacters*/ nil, /*labelDetails*/ file, position, false, /*isMemberCompletion*/ false, /*isSnippet*/ false, /*hasAction*/ false, /*preselect*/ "", /*source*/ nil, /*autoImportEntryData*/ nil, /*detail*/ ) items = append(items, &CompletionItem{ CompletionItem: lspItem, }) } } 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) } tokenKind := keywordForNode(contextToken) // If the previous token is keyword corresponding to class member completion keyword // there will be completion available here if isClassMemberCompletionKeyword(tokenKind) && 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(tokenKind) || 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.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 } } if tokenKind == ast.KindConstKeyword { 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 clientSupportsItemLabelDetails(ctx context.Context) bool { return lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionItem.LabelDetailsSupport } func clientSupportsItemSnippet(ctx context.Context) bool { return lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionItem.SnippetSupport } func clientSupportsItemCommitCharacters(ctx context.Context) bool { return lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionItem.CommitCharactersSupport } func clientSupportsItemInsertReplace(ctx context.Context) bool { return lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionItem.InsertReplaceSupport } func clientSupportsDefaultCommitCharacters(ctx context.Context) bool { return slices.Contains(lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionList.ItemDefaults, "commitCharacters") } func clientSupportsDefaultEditRange(ctx context.Context) bool { return slices.Contains(lsproto.GetClientCapabilities(ctx).TextDocument.Completion.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, } } // 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 *lsproto.CompletionItemData, ) (*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) } checker, done := program.GetTypeCheckerForFile(ctx, file) defer done() return l.getCompletionItemDetails(ctx, program, checker, int(data.Position), file, item, data), nil } func getCompletionDocumentationFormat(ctx context.Context) lsproto.MarkupKind { return lsproto.PreferredMarkupKind(lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionItem.DocumentationFormat) } func (l *LanguageService) getCompletionItemDetails( ctx context.Context, program *compiler.Program, checker *checker.Checker, position int, file *ast.SourceFile, item *lsproto.CompletionItem, data *lsproto.CompletionItemData, ) *lsproto.CompletionItem { docFormat := getCompletionDocumentationFormat(ctx) contextToken, previousToken := getRelevantTokens(position, file) if IsInString(file, position, previousToken) { return l.getStringLiteralCompletionDetails( ctx, checker, item, data.Name, file, position, contextToken, docFormat, ) } if data.AutoImport != nil { edits, description := (&autoimport.Fix{AutoImportFix: data.AutoImport}).Edits(ctx, file, program.Options(), l.FormatOptions(), l.converters, l.UserPreferences()) item.AdditionalTextEdits = &edits item.Detail = strPtrTo(description) return item } // Compute all the completion symbols again. symbolCompletion := l.getSymbolCompletionFromItemData( ctx, checker, file, position, data, ) preferences := l.UserPreferences() switch { case symbolCompletion.request != nil: request := *symbolCompletion.request switch request := request.(type) { case *completionDataJSDocTagName: return createSimpleDetails(item, data.Name, docFormat) case *completionDataJSDocTag: return createSimpleDetails(item, data.Name, docFormat) case *completionDataJSDocParameterName: return createSimpleDetails(item, data.Name, docFormat) case *completionDataKeyword: if core.Some(request.keywordCompletions, func(c *CompletionItem) bool { return c.Label == data.Name }) { return createSimpleDetails(item, data.Name, docFormat) } return item default: panic(fmt.Sprintf("Unexpected completion data type: %T", request)) } case symbolCompletion.symbol != nil: symbolDetails := symbolCompletion.symbol return l.createCompletionDetailsForSymbol( item, symbolDetails.symbol, checker, symbolDetails.location, position, docFormat, ) case symbolCompletion.literal != nil: literal := symbolCompletion.literal return createSimpleDetails(item, completionNameForLiteral(file, preferences, *literal), docFormat) case symbolCompletion.cases != nil: 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 == data.Name }) { return createSimpleDetails(item, data.Name, docFormat) } 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 *lsproto.CompletionItemData, ) detailsData { if itemData.Source == SourceSwitchCases { return detailsData{ cases: &struct{}{}, } } completionData, err := l.getCompletionData(ctx, ch, file, position, l.UserPreferences(), true /*forItemResolve*/) if err != nil { panic(err) } if completionData == nil { return detailsData{} } if _, ok := completionData.(*completionDataData); !ok { return detailsData{ request: &completionData, } } data := completionData.(*completionDataData) preferences := l.UserPreferences() 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 index, symbol := range data.symbols { origin := data.symbolToOriginInfoMap[index] 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 createSimpleDetails( item *lsproto.CompletionItem, name string, docFormat lsproto.MarkupKind, ) *lsproto.CompletionItem { return createCompletionDetails(item, name, "" /*documentation*/, docFormat) } func createCompletionDetails( item *lsproto.CompletionItem, detail string, documentation string, docFormat lsproto.MarkupKind, ) *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: docFormat, 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 (l *LanguageService) createCompletionDetailsForSymbol( item *lsproto.CompletionItem, symbol *ast.Symbol, checker *checker.Checker, location *ast.Node, position int, docFormat lsproto.MarkupKind, ) *lsproto.CompletionItem { quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(checker, symbol, location, docFormat, nil) return createCompletionDetails(item, quickInfo, documentation, docFormat) } 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) { if importClause := candidate.ImportClause(); importClause != nil { result.isTopLevelTypeOnly = 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) // Use token position (excluding JSDoc/trivia) instead of node.Pos() to avoid including JSDoc comments tokenPos := scanner.GetTokenPosOfNode(node, sourceFile, false /*includeJSDoc*/) if printer.GetLinesBetweenPositions(sourceFile, tokenPos, node.End()) == 0 { return new(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 new(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 == nil || namedBindings.Kind != ast.KindNamedImports { return nil } return core.Find(namedBindings.Elements(), func(e *ast.Node) bool { return e.PropertyName() == nil && lsutil.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.Node { 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.Node) *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.Node) bool { switch tag.Kind { case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag, ast.KindJSDocReturnTag, ast.KindJSDocTypeTag, ast.KindJSDocTypedefTag, ast.KindJSDocThrowsTag, ast.KindJSDocSatisfiesTag: return true case ast.KindJSDocTemplateTag: return tag.AsJSDocTemplateTag().Constraint != nil default: return false } } func (l *LanguageService) jsDocCompletionInfo( ctx context.Context, position int, file *ast.SourceFile, items []*CompletionItem, ) *CompletionList { defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) itemDefaults := l.setItemDefaults( ctx, position, file, items, &defaultCommitCharacters, nil, /*optionalReplacementSpan*/ ) return &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: new(lsproto.CompletionItemKindKeyword), SortText: new(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: new(lsproto.CompletionItemKindKeyword), SortText: new(string(SortTextLocationPriority)), } items = append(items, item) } return items }) func getJSDocTagNameCompletions() []*CompletionItem { return cloneItems(jsDocTagNameCompletionItems()) } func getJSDocTagCompletions() []*CompletionItem { return cloneItems(jsDocTagCompletionItems()) } func getJSDocParameterCompletions( ctx context.Context, file *ast.SourceFile, position int, typeChecker *checker.Checker, options *core.CompilerOptions, preferences lsutil.UserPreferences, tagNameOnly bool, ) []*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.Node 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) *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 &CompletionItem{ CompletionItem: &lsproto.CompletionItem{ Label: displayText, Kind: new(lsproto.CompletionItemKindVariable), SortText: new(string(SortTextLocationPriority)), InsertText: strPtrTo(snippetText), InsertTextFormat: core.IfElse(isSnippet, new(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 &CompletionItem{ CompletionItem: &lsproto.CompletionItem{ Label: displayText, Kind: new(lsproto.CompletionItemKindVariable), SortText: new(string(SortTextLocationPriority)), InsertText: strPtrTo(snippetText), InsertTextFormat: core.IfElse(isSnippet, new(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 lsutil.UserPreferences, tabstopCounter *int, ) string { if isSnippet { debug.Assert(tabstopCounter != nil) } if initializer != nil { paramName = getJSDocParamNameWithInitializer(paramName, initializer) } if isSnippet { paramName = escapeSnippetText(paramName) } if isJS { t := "*" if isObject { debug.Assert(dotDotDotToken == nil, `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 := lsutil.GetQuotePreference(file, preferences) builderFlags := core.IfElse( quotePreference == lsutil.QuotePreferenceSingle, nodebuilder.FlagsUseSingleQuotesForStringLiteralType, nodebuilder.FlagsNone, ) typeNode := typeChecker.TypeToTypeNode( inferredType, ast.FindAncestor(initializer, ast.IsFunctionLike), builderFlags, nil, /*idToSymbol*/ ) 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 lsutil.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 lsutil.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 lsutil.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.JSDocParameterOrPropertyTag) []*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.Node if jsDoc.AsJSDoc().Tags != nil { tags = jsDoc.AsJSDoc().Tags.Nodes } return core.MapNonNil(fn.Parameters(), func(param *ast.ParameterDeclarationNode) *CompletionItem { if !ast.IsIdentifier(param.Name()) { return nil } name := param.Name().Text() if core.Some(tags, func(t *ast.Node) bool { return t != tag.AsNode() && ast.IsJSDocParameterTag(t) && ast.IsIdentifier(t.Name()) && t.Name().Text() == name }) || nameThusFar != "" && !strings.HasPrefix(name, nameThusFar) { return nil } return &CompletionItem{ CompletionItem: &lsproto.CompletionItem{ Label: name, Kind: new(lsproto.CompletionItemKindVariable), SortText: new(string(SortTextLocationPriority)), }, } }) } func (l *LanguageService) getExhaustiveCaseSnippets( ctx context.Context, caseBlock *ast.CaseBlock, file *ast.SourceFile, position int, options *core.CompilerOptions, program *compiler.Program, c *checker.Checker, ) (*lsproto.CompletionItem, error) { clauses := caseBlock.Clauses.Nodes switchType := c.GetTypeAtLocation(caseBlock.AsNode().Parent.Expression()) if switchType != nil && switchType.IsUnion() && core.Every(switchType.Types(), isLiteral) { // Collect constant values in existing clauses. tracker := newCaseClauseTracker(c, clauses) target := options.GetEmitScriptTarget() quotePreference := lsutil.GetQuotePreference(file, l.UserPreferences()) // Tolerate a nil import adder in untitled files. var importAdder autoimport.ImportAdder if !tspath.IsDynamicFileName(file.FileName()) { view, err := l.getPreparedAutoImportView(file) if err != nil { return nil, err } if view != nil { importAdder = autoimport.NewImportAdder( ctx, program, c, file, view, l.FormatOptions(), l.converters, l.UserPreferences(), ) } } var elements []*ast.Expression factory := ast.NewNodeFactory(ast.NodeFactoryHooks{}) for _, t := range switchType.Types() { // Enums if t.IsEnumLiteral() { debug.Assert(t.Symbol() != nil, "An enum member type should have a symbol") debug.Assert(t.Symbol().Parent != nil, "An enum member type should have a parent symbol (the enum symbol)") // Filter existing enums by their values var enumValue any if t.Symbol().ValueDeclaration != nil { enumValue = c.GetConstantValue(t.Symbol().ValueDeclaration) } if enumValue != nil { if tracker.hasValue(enumValue) { continue } tracker.addValue(enumValue) } typeNode := autoimport.TypeToAutoImportableTypeNode(c, importAdder, t, caseBlock.AsNode()) if typeNode == nil { return nil, nil } expr := typeNodeToExpression(typeNode, target, quotePreference, factory) if expr == nil { return nil, nil } elements = append(elements, expr) } else if value := t.AsLiteralType().Value(); !tracker.hasValue(value) { // Literals switch v := value.(type) { case jsnum.PseudoBigInt: var bigInt *ast.Node if v.Negative { v.Negative = false bigInt = factory.NewPrefixUnaryExpression(ast.KindMinusToken, factory.NewBigIntLiteral(v.String()+"n", ast.TokenFlagsNone)) } else { bigInt = factory.NewBigIntLiteral(v.String()+"n", ast.TokenFlagsNone) } elements = append(elements, bigInt) case jsnum.Number: var number *ast.Node if v < 0 { number = factory.NewPrefixUnaryExpression(ast.KindMinusToken, factory.NewNumericLiteral(v.Abs().String(), ast.TokenFlagsNone)) } else { number = factory.NewNumericLiteral(v.String(), ast.TokenFlagsNone) } elements = append(elements, number) case string: literal := factory.NewStringLiteral(v, core.IfElse(quotePreference == lsutil.QuotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone)) elements = append(elements, literal) } } } if len(elements) == 0 { return nil, nil } newClauses := core.Map(elements, func(element *ast.Node) *ast.CaseOrDefaultClauseNode { return factory.NewCaseOrDefaultClause(ast.KindCaseClause, element, factory.NewNodeList(nil)) }) newLineChar := l.FormatOptions().NewLineCharacter printer := createSnippetPrinter(printer.PrinterOptions{ RemoveComments: true, NewLine: core.GetNewLineKind(newLineChar), }) printNode := func(node *ast.Node) string { return printer.printAndFormatNode(ctx, node, file) } insertText := strings.Join(core.MapIndex(newClauses, func(clause *ast.Node, i int) string { if clientSupportsItemSnippet(ctx) { return fmt.Sprintf("%s$%d", printNode(clause), i+1) } return printer.printUnescapedNode(clause) }), newLineChar) firstClause := printer.printUnescapedNode(newClauses[0]) name := firstClause + " ..." var additionalTextEdits *[]*lsproto.TextEdit if importAdder != nil { additionalTextEdits = new(importAdder.Edits()) } return &lsproto.CompletionItem{ Label: name, Kind: new(lsproto.CompletionItemKindSnippet), SortText: new(string(SortTextGlobalsOrKeywords)), InsertText: strPtrTo(insertText), AdditionalTextEdits: additionalTextEdits, InsertTextFormat: core.IfElse(clientSupportsItemSnippet(ctx), new(lsproto.InsertTextFormatSnippet), nil), Data: &lsproto.CompletionItemData{ FileName: file.FileName(), Position: int32(position), Name: name, Source: string(completionSourceSwitchCases), }, }, nil } return nil, nil } func typeNodeToExpression( typeNode *ast.TypeNode, target core.ScriptTarget, quotePreference lsutil.QuotePreference, factory *ast.NodeFactory, ) *ast.Expression { switch typeNode.Kind { case ast.KindTypeReference: typeName := typeNode.AsTypeReferenceNode().TypeName return entityNameToExpression(typeName, target, quotePreference, factory) case ast.KindIndexedAccessType: objectExpression := typeNodeToExpression( typeNode.AsIndexedAccessTypeNode().ObjectType, target, quotePreference, factory, ) indexExpression := typeNodeToExpression( typeNode.AsIndexedAccessTypeNode().IndexType, target, quotePreference, factory, ) if objectExpression != nil && indexExpression != nil { return factory.NewElementAccessExpression(objectExpression, nil /*questionDotToken*/, indexExpression, ast.NodeFlagsNone) } return nil case ast.KindLiteralType: literal := typeNode.AsLiteralTypeNode().Literal switch literal.Kind { case ast.KindStringLiteral: expr := factory.NewStringLiteral(literal.Text(), core.IfElse(quotePreference == lsutil.QuotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone)) return expr case ast.KindNumericLiteral: expr := factory.NewNumericLiteral(literal.Text(), literal.AsNumericLiteral().TokenFlags) return expr default: return nil } case ast.KindParenthesizedType: expr := typeNodeToExpression( typeNode.AsParenthesizedTypeNode().Type, target, quotePreference, factory, ) if expr == nil { return nil } if ast.IsIdentifier(expr) { return expr } return factory.NewParenthesizedExpression(expr) case ast.KindTypeQuery: return entityNameToExpression(typeNode.AsTypeQueryNode().ExprName, target, quotePreference, factory) case ast.KindImportType: debug.Fail(`We should not get an import type after calling 'typeToAutoImportableTypeNode'.`) return nil } return nil } func entityNameToExpression( entityName *ast.EntityName, target core.ScriptTarget, quotePreference lsutil.QuotePreference, factory *ast.NodeFactory, ) *ast.Expression { if ast.IsIdentifier(entityName) { return entityName } return factory.NewPropertyAccessExpression( entityNameToExpression(entityName.AsQualifiedName().Left, target, quotePreference, factory), nil, /*questionDotToken*/ entityName.AsQualifiedName().Right, ast.NodeFlagsNone, ) } type snippetPrinter struct { baseWriter *printer.ChangeTrackerWriter printer *printer.Printer writer *snippetEmitTextWriter factory *ast.NodeFactory } /** Snippet-escaping version of `printer.printNode`. */ func (p *snippetPrinter) printNode(node *ast.Node) string { unescaped := p.printUnescapedNode(node) if len(p.writer.escapes) > 0 { return core.ApplyBulkEdits(unescaped, p.writer.escapes) } return unescaped } func (p *snippetPrinter) printUnescapedNode(node *ast.Node) string { p.writer.escapes = nil p.writer.Clear() p.printer.Write(node, nil /*sourceFile*/, p.writer, nil /*sourceMapGenerator*/) return p.writer.String() } func (p *snippetPrinter) printAndFormatNode(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile) string { text := p.printUnescapedNode(node) nodeWithPos := p.baseWriter.AssignPositionsToNode(node, p.factory) syntheticFile := p.createSyntheticFile(nodeWithPos, text, sourceFile) changes := format.FormatNodeGivenIndentation( ctx, nodeWithPos, syntheticFile, sourceFile.LanguageVariant, 0, /*initialIndentation*/ 0, /*delta*/ ) allChanges := changes if len(p.writer.escapes) > 0 { allChanges = append(changes, p.writer.escapes...) slices.SortFunc(allChanges, func(a, b core.TextChange) int { return core.CompareTextRanges(a.TextRange, b.TextRange) }) } return core.ApplyBulkEdits(syntheticFile.Text(), allChanges) } // Creates a source file containing `node` for formatting purposes. // `node` and descendants need to be synthetic nodes with positions assigned. // This function also assigns parent pointers. func (p *snippetPrinter) createSyntheticFile(node *ast.Node, text string, targetFile *ast.SourceFile) *ast.SourceFile { eof := p.factory.NewToken(ast.KindEndOfFile) eof.Loc = core.NewTextRange(len(text), len(text)) statements := p.factory.NewNodeList([]*ast.Node{node}) statements.Loc = core.NewTextRange(node.Pos(), node.End()) syntheticFile := p.factory.NewSourceFile( targetFile.ParseOptions(), text, statements, eof, ) syntheticFile.Loc = core.NewTextRange(0, len(text)) ast.SetParentInChildren(syntheticFile) return syntheticFile.AsSourceFile() } func createSnippetPrinter(options printer.PrinterOptions) *snippetPrinter { baseWriter := printer.NewChangeTrackerWriter(options.NewLine.GetNewLineCharacter(), -1) printer := printer.NewPrinter(options, baseWriter.GetPrintHandlers(), nil /*emitContext*/) writer := &snippetEmitTextWriter{ ChangeTrackerWriter: baseWriter, } return &snippetPrinter{ baseWriter: baseWriter, printer: printer, writer: writer, factory: ast.NewNodeFactory(ast.NodeFactoryHooks{}), } } // Override base writer methods to perform snippet escaping. type snippetEmitTextWriter struct { *printer.ChangeTrackerWriter escapes []core.TextChange } func (w *snippetEmitTextWriter) nonEscapingWrite(s string) { w.ChangeTrackerWriter.Write(s) } func (w *snippetEmitTextWriter) Write(s string) { w.escapingWrite(s, func() { w.ChangeTrackerWriter.Write(s) }) } func (w *snippetEmitTextWriter) WriteComment(text string) { w.escapingWrite(text, func() { w.ChangeTrackerWriter.WriteComment(text) }) } func (w *snippetEmitTextWriter) WriteStringLiteral(text string) { w.escapingWrite(text, func() { w.ChangeTrackerWriter.WriteStringLiteral(text) }) } func (w *snippetEmitTextWriter) WriteParameter(text string) { w.escapingWrite(text, func() { w.ChangeTrackerWriter.WriteParameter(text) }) } func (w *snippetEmitTextWriter) WriteProperty(text string) { w.escapingWrite(text, func() { w.ChangeTrackerWriter.WriteProperty(text) }) } func (w *snippetEmitTextWriter) WriteSymbol(text string, symbol *ast.Symbol) { w.escapingWrite(text, func() { w.ChangeTrackerWriter.WriteSymbol(text, symbol) }) } // The formatter/scanner will have issues with snippet-escaped text, // so instead of writing the escaped text directly to the writer, // generate a set of changes that can be applied to the unescaped text // to escape it post-formatting. func (w *snippetEmitTextWriter) escapingWrite(s string, write func()) { escaped := escapeSnippetText(s) if escaped != s { start := w.GetTextPos() write() end := w.GetTextPos() w.escapes = append( w.escapes, core.TextChange{ NewText: escaped, TextRange: core.NewTextRange(start, end), }, ) } else { write() } }