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