package ls

import (
	"context"
	"errors"
	"fmt"
	"slices"
	"strings"
	"sync"
	"unicode"
	"unicode/utf8"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/astnav"
	"github.com/microsoft/typescript-go/internal/checker"
	"github.com/microsoft/typescript-go/internal/collections"
	"github.com/microsoft/typescript-go/internal/compiler"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/debug"
	"github.com/microsoft/typescript-go/internal/format"
	"github.com/microsoft/typescript-go/internal/jsnum"
	"github.com/microsoft/typescript-go/internal/ls/autoimport"
	"github.com/microsoft/typescript-go/internal/ls/lsutil"
	"github.com/microsoft/typescript-go/internal/lsp/lsproto"
	"github.com/microsoft/typescript-go/internal/nodebuilder"
	"github.com/microsoft/typescript-go/internal/printer"
	"github.com/microsoft/typescript-go/internal/scanner"
	"github.com/microsoft/typescript-go/internal/stringutil"
	"github.com/microsoft/typescript-go/internal/tspath"
)

var ErrNeedsAutoImports = errors.New("completion list needs auto imports")

func (l *LanguageService) ProvideCompletion(
	ctx context.Context,
	documentURI lsproto.DocumentUri,
	LSPPosition lsproto.Position,
	context *lsproto.CompletionContext,
) (lsproto.CompletionResponse, error) {
	_, file := l.getProgramAndFile(documentURI)
	var triggerCharacter *string
	if context != nil {
		triggerCharacter = context.TriggerCharacter
	}
	ctx = format.WithFormatCodeSettings(ctx, l.FormatOptions(), l.FormatOptions().NewLineCharacter)
	position := int(l.converters.LineAndCharacterToPosition(file, LSPPosition))
	completionListInternal, err := l.getCompletionsAtPosition(
		ctx,
		file,
		position,
		triggerCharacter,
		false, /*includeSymbols*/
	)
	if err != nil {
		return lsproto.CompletionItemsOrListOrNull{}, err
	}
	completionList := ensureItemData(file.FileName(), position, completionListInternal.toLSP())
	return lsproto.CompletionItemsOrListOrNull{List: completionList}, nil
}

func (l *LanguageService) GetCompletionsAtPosition(ctx context.Context, file *ast.SourceFile, position int, triggerCharacter *string, includeSymbols bool) (*CompletionList, error) {
	return l.getCompletionsAtPosition(ctx, file, position, triggerCharacter, includeSymbols)
}

type CompletionItem struct {
	*lsproto.CompletionItem
	Symbol *ast.Symbol // non-nil for symbol completions when IncludeSymbols is set; nil otherwise
}

type CompletionList struct {
	IsIncomplete bool
	ItemDefaults *lsproto.CompletionItemDefaults
	ApplyKind    *lsproto.CompletionItemApplyKinds
	Items        []*CompletionItem
}

func ensureItemData(fileName string, pos int, list *lsproto.CompletionList) *lsproto.CompletionList {
	if list == nil {
		return nil
	}
	for _, item := range list.Items {
		if item.Data == nil {
			item.Data = &lsproto.CompletionItemData{
				FileName: fileName,
				Position: int32(pos),
				Name:     item.Label,
			}
		}
	}
	return list
}

// *completionDataData | *completionDataKeyword | *completionDataJSDocTagName | *completionDataJSDocTag | *completionDataJSDocParameterName
type completionData = any

type completionDataData struct {
	symbols          []*ast.Symbol
	autoImports      []*autoimport.FixAndExport
	completionKind   CompletionKind
	isInSnippetScope bool
	// Note that the presence of this alone doesn't mean that we need a conversion. Only do that if the completion is not an ordinary identifier.
	propertyAccessToConvert      *ast.PropertyAccessExpressionNode
	isNewIdentifierLocation      bool
	location                     *ast.Node
	keywordFilters               KeywordCompletionFilters
	literals                     []literalValue
	symbolToOriginInfoMap        map[int]*symbolOriginInfo
	symbolToSortTextMap          map[ast.SymbolId]SortText
	recommendedCompletion        *ast.Symbol
	previousToken                *ast.Node
	contextToken                 *ast.Node
	jsxInitializer               jsxInitializer
	insideJSDocTagTypeExpression bool
	isTypeOnlyLocation           bool
	// In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier.
	isJsxIdentifierExpected   bool
	isRightOfOpenTag          bool
	isRightOfDotOrQuestionDot bool
	importStatementCompletion *importStatementCompletionInfo // !!!
	hasUnresolvedAutoImports  bool                           // !!!
	// flags CompletionInfoFlags // !!!
	defaultCommitCharacters []string
}

type completionDataKeyword struct {
	keywordCompletions      []*CompletionItem
	isNewIdentifierLocation bool
}

type completionDataJSDocTagName struct{}

type completionDataJSDocTag struct{}

type completionDataJSDocParameterName struct {
	tag *ast.JSDocParameterOrPropertyTag
}

type importStatementCompletionInfo struct {
	isKeywordOnlyCompletion        bool
	keywordCompletion              ast.Kind // TokenKind
	isNewIdentifierLocation        bool
	isTopLevelTypeOnly             bool
	couldBeTypeOnlyImportSpecifier bool
	replacementSpan                *lsproto.Range
}

// If we're after the `=` sign but no identifier has been typed yet,
// value will be `true` but initializer will be `nil`.
type jsxInitializer struct {
	isInitializer bool
	initializer   *ast.IdentifierNode
}

type KeywordCompletionFilters int

const (
	KeywordCompletionFiltersNone                         KeywordCompletionFilters = iota // No keywords
	KeywordCompletionFiltersAll                                                          // Every possible kewyord
	KeywordCompletionFiltersClassElementKeywords                                         // Keywords inside class body
	KeywordCompletionFiltersInterfaceElementKeywords                                     // Keywords inside interface body
	KeywordCompletionFiltersConstructorParameterKeywords                                 // Keywords at constructor parameter
	KeywordCompletionFiltersFunctionLikeBodyKeywords                                     // Keywords at function like body
	KeywordCompletionFiltersTypeAssertionKeywords
	KeywordCompletionFiltersTypeKeywords
	KeywordCompletionFiltersTypeKeyword // Literally just `type`
	KeywordCompletionFiltersLast        = KeywordCompletionFiltersTypeKeyword
)

func keywordFiltersFromSyntaxKind(keywordCompletion ast.Kind) KeywordCompletionFilters {
	switch keywordCompletion {
	case ast.KindTypeKeyword:
		return KeywordCompletionFiltersTypeKeyword
	default:
		panic("Unknown mapping from ast.Kind `" + keywordCompletion.String() + "` to KeywordCompletionFilters")
	}
}

type CompletionKind int

const (
	CompletionKindNone CompletionKind = iota
	CompletionKindObjectPropertyDeclaration
	CompletionKindGlobal
	CompletionKindPropertyAccess
	CompletionKindMemberLike
	CompletionKindString
)

var TriggerCharacters = []string{".", `"`, "'", "`", "/", "@", "<", "#", " ", "*"}

// All commit characters, valid when `isNewIdentifierLocation` is false.
var allCommitCharacters = []string{".", ",", ";"}

// Commit characters valid at expression positions where we could be inside a parameter list.
var noCommaCommitCharacters = []string{".", ";"}

var emptyCommitCharacters = []string{}

type SortText string

const (
	SortTextLocalDeclarationPriority         SortText = "10"
	SortTextLocationPriority                 SortText = "11"
	SortTextOptionalMember                   SortText = "12"
	SortTextMemberDeclaredBySpreadAssignment SortText = "13"
	SortTextSuggestedClassMembers            SortText = "14"
	SortTextGlobalsOrKeywords                SortText = "15"
	SortTextAutoImportSuggestions            SortText = "16"
	SortTextClassMemberSnippets              SortText = "17"
	SortTextJavascriptIdentifiers            SortText = "18"
)

func DeprecateSortText(original SortText) SortText {
	return "z" + original
}

func sortBelow(original SortText) SortText {
	return original + "1"
}

type symbolOriginInfoKind int

const (
	symbolOriginInfoKindThisType symbolOriginInfoKind = 1 << iota
	symbolOriginInfoKindSymbolMember
	symbolOriginInfoKindPromise
	symbolOriginInfoKindNullable
	symbolOriginInfoKindTypeOnlyAlias
	symbolOriginInfoKindObjectLiteralMethod
	symbolOriginInfoKindIgnore
	symbolOriginInfoKindComputedPropertyName
)

type symbolOriginInfo struct {
	kind              symbolOriginInfoKind
	isDefaultExport   bool
	isFromPackageJson bool
	fileName          string
	data              any
}

func (origin *symbolOriginInfo) symbolName() string {
	switch origin.data.(type) {
	case *symbolOriginInfoComputedPropertyName:
		return origin.data.(*symbolOriginInfoComputedPropertyName).symbolName
	default:
		panic(fmt.Sprintf("symbolOriginInfo: unknown data type for symbolName(): %T", origin.data))
	}
}

type symbolOriginInfoObjectLiteralMethod struct {
	insertText   string
	labelDetails *lsproto.CompletionItemLabelDetails
	isSnippet    bool
}

func (s *symbolOriginInfo) asObjectLiteralMethod() *symbolOriginInfoObjectLiteralMethod {
	return s.data.(*symbolOriginInfoObjectLiteralMethod)
}

type symbolOriginInfoTypeOnlyAlias struct {
	declaration *ast.TypeOnlyImportDeclaration
}

type symbolOriginInfoComputedPropertyName struct {
	symbolName string
}

// Special values for `CompletionInfo['source']` used to disambiguate
// completion items with the same `name`. (Each completion item must
// have a unique name/source combination, because those two fields
// comprise `CompletionEntryIdentifier` in `getCompletionEntryDetails`.
//
// When the completion item is an auto-import suggestion, the source
// is the module specifier of the suggestion. To avoid collisions,
// the values here should not be a module specifier we would ever
// generate for an auto-import.
type completionSource string

const (
	// Completions that require `this.` insertion text.
	completionSourceThisProperty completionSource = "ThisProperty/"
	// Auto-import that comes attached to a class member snippet.
	completionSourceClassMemberSnippet completionSource = "ClassMemberSnippet/"
	// A type-only import that needs to be promoted in order to be used at the completion location.
	completionSourceTypeOnlyAlias completionSource = "TypeOnlyAlias/"
	// Auto-import that comes attached to an object literal method snippet.
	completionSourceObjectLiteralMethodSnippet completionSource = "ObjectLiteralMethodSnippet/"
	// Case completions for switch statements.
	completionSourceSwitchCases completionSource = "SwitchCases/"
	// Completions for an object literal expression.
	completionSourceObjectLiteralMemberWithComma completionSource = "ObjectLiteralMemberWithComma/"
)

// Value is set to false for global variables or completions from external module exports,
// true otherwise.
type uniqueNamesMap = map[string]bool

// string | jsnum.Number | PseudoBigInt
type literalValue any

type globalsSearch int

const (
	globalsSearchContinue globalsSearch = iota
	globalsSearchSuccess
	globalsSearchFail
)

func (l *CompletionList) toLSP() *lsproto.CompletionList {
	if l == nil {
		return nil
	}
	items := make([]*lsproto.CompletionItem, 0, len(l.Items))
	for _, entry := range l.Items {
		if entry != nil && entry.CompletionItem != nil {
			items = append(items, entry.CompletionItem)
		}
	}
	return &lsproto.CompletionList{
		IsIncomplete: l.IsIncomplete,
		ItemDefaults: l.ItemDefaults,
		ApplyKind:    l.ApplyKind,
		Items:        items,
	}
}

func (l *LanguageService) getCompletionsAtPosition(
	ctx context.Context,
	file *ast.SourceFile,
	position int,
	triggerCharacter *string,
	includeSymbols bool,
) (*CompletionList, error) {
	_, previousToken := getRelevantTokens(position, file)
	if triggerCharacter != nil && !IsInString(file, position, previousToken) && !isValidTrigger(file, *triggerCharacter, previousToken, position) {
		return nil, nil
	}

	if triggerCharacter != nil && *triggerCharacter == " " {
		// `isValidTrigger` ensures we are at `import |`
		if l.UserPreferences().IncludeCompletionsForImportStatements.IsTrue() {
			return &CompletionList{IsIncomplete: true}, nil
		}
		return nil, nil
	}

	if jsDocSnippetCompletion := l.getJSDocSnippetCompletion(ctx, file, position); jsDocSnippetCompletion != nil {
		return jsDocSnippetCompletion, nil
	}

	compilerOptions := l.GetProgram().Options()

	// !!! see if incomplete completion list and continue or clean

	checker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file)
	defer done()

	stringCompletions := l.getStringLiteralCompletions(
		ctx,
		file,
		position,
		previousToken,
		checker,
		compilerOptions,
		includeSymbols,
	)
	if stringCompletions != nil {
		return stringCompletions, nil
	}

	if previousToken != nil && (previousToken.Kind == ast.KindBreakKeyword ||
		previousToken.Kind == ast.KindContinueKeyword ||
		previousToken.Kind == ast.KindIdentifier) &&
		ast.IsBreakOrContinueStatement(previousToken.Parent) {
		return l.getLabelCompletionsAtPosition(
			ctx,
			previousToken.Parent,
			file,
			position,
			l.getOptionalReplacementSpan(previousToken, file),
		), nil
	}

	preferences := l.UserPreferences()
	data, err := l.getCompletionData(ctx, checker, file, position, preferences, false /*forItemResolve*/)
	if err != nil {
		return nil, err
	}
	if data == nil {
		return nil, nil
	}

	switch data := data.(type) {
	case *completionDataData:
		optionalReplacementSpan := l.getOptionalReplacementSpan(data.location, file)
		response, err := l.completionInfoFromData(
			ctx,
			checker,
			file,
			compilerOptions,
			data,
			position,
			optionalReplacementSpan,
			includeSymbols,
		)
		if err != nil {
			return nil, err
		}
		return response, nil
	case *completionDataKeyword:
		optionalReplacementSpan := l.getOptionalReplacementSpan(previousToken, file)
		return l.specificKeywordCompletionInfo(
			ctx,
			position,
			file,
			data.keywordCompletions,
			data.isNewIdentifierLocation,
			optionalReplacementSpan,
		), nil
	case *completionDataJSDocTagName:
		// If the current position is a jsDoc tag name, only tag names should be provided for completion
		items := getJSDocTagNameCompletions()
		items = append(items, getJSDocParameterCompletions(
			ctx,
			file,
			position,
			checker,
			compilerOptions,
			preferences,
			/*tagNameOnly*/ true,
		)...)
		return l.jsDocCompletionInfo(ctx, position, file, items), nil
	case *completionDataJSDocTag:
		// If the current position is a jsDoc tag, only tags should be provided for completion
		items := getJSDocTagCompletions()
		items = append(items, getJSDocParameterCompletions(
			ctx,
			file,
			position,
			checker,
			compilerOptions,
			preferences,
			/*tagNameOnly*/ false,
		)...)
		return l.jsDocCompletionInfo(ctx, position, file, items), nil
	case *completionDataJSDocParameterName:
		return l.jsDocCompletionInfo(ctx, position, file, getJSDocParameterNameCompletions(data.tag)), nil
	default:
		panic("getCompletionData() returned unexpected type: " + fmt.Sprintf("%T", data))
	}
}

func (l *LanguageService) getCompletionData(
	ctx context.Context,
	typeChecker *checker.Checker,
	file *ast.SourceFile,
	position int,
	preferences lsutil.UserPreferences,
	forItemResolve bool,
) (completionData, error) {
	inCheckedFile := isCheckedFile(file, l.GetProgram().Options())

	currentToken := astnav.GetTokenAtPosition(file, position)

	insideComment := isInComment(file, position, currentToken)

	insideJSDocTagTypeExpression := false
	insideJsDocImportTag := false
	isInSnippetScope := false
	if insideComment != nil {
		if hasDocComment(file, position) {
			if position > 0 && file.Text()[position-1] == '@' {
				// The current position is next to the '@' sign, when no tag name being provided yet.
				// Provide a full list of tag names
				return &completionDataJSDocTagName{}, nil
			} else {
				// When completion is requested without "@", we will have check to make sure that
				// there are no comments prefix the request position. We will only allow "*" and space.
				// e.g
				//   /** |c| /*
				//
				//   /**
				//     |c|
				//    */
				//
				//   /**
				//    * |c|
				//    */
				//
				//   /**
				//    *         |c|
				//    */
				lineStart := format.GetLineStartPositionForPosition(position, file)
				noCommentPrefix := true
				for _, r := range file.Text()[lineStart:position] {
					if !(stringutil.IsWhiteSpaceSingleLine(r) || r == '*' || r == '/' || r == '(' || r == ')' || r == '|') {
						noCommentPrefix = false
						break
					}
				}
				if noCommentPrefix {
					return &completionDataJSDocTag{}, nil
				}
			}
		}

		// Completion should work inside certain JSDoc tags. For example:
		//     /** @type {number | string} */
		// Completion should work in the brackets
		if tag := getJSDocTagAtPosition(currentToken, position); tag != nil {
			if tag.TagName().Pos() <= position && position <= tag.TagName().End() {
				return &completionDataJSDocTagName{}, nil
			}
			if ast.IsJSDocImportTag(tag) {
				insideJsDocImportTag = true
			} else {
				if typeExpression := tryGetTypeExpressionFromTag(tag); typeExpression != nil {
					currentToken = astnav.GetTokenAtPosition(file, position)
					if currentToken == nil ||
						(!ast.IsDeclarationName(currentToken) &&
							(currentToken.Parent.Kind != ast.KindJSDocPropertyTag ||
								currentToken.Parent.Name() != currentToken)) {
						// Use as type location if inside tag's type expression
						insideJSDocTagTypeExpression = isCurrentlyEditingNode(typeExpression, file, position)
					}
				}
				if !insideJSDocTagTypeExpression &&
					ast.IsJSDocParameterTag(tag) &&
					(ast.NodeIsMissing(tag.Name()) || tag.Name().Pos() <= position && position <= tag.Name().End()) {
					return &completionDataJSDocParameterName{
						tag: tag.AsJSDocParameterOrPropertyTag(),
					}, nil
				}
			}
		}

		if !insideJSDocTagTypeExpression && !insideJsDocImportTag {
			// Proceed if the current position is in JSDoc tag expression; otherwise it is a normal
			// comment or the plain text part of a JSDoc comment, so no completion should be available
			return nil, nil
		}
	}

	// The decision to provide completion depends on the contextToken, which is determined through the previousToken.
	// Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file
	isJSOnlyLocation := !insideJSDocTagTypeExpression && !insideJsDocImportTag && ast.IsSourceFileJS(file)
	contextToken, previousToken := getRelevantTokens(position, file)

	// Find the node where completion is requested on.
	// Also determine whether we are trying to complete with members of that node
	// or attributes of a JSX tag.
	node := currentToken
	var propertyAccessToConvert *ast.PropertyAccessExpressionNode
	isRightOfDot := false
	isRightOfQuestionDot := false
	isRightOfOpenTag := false
	isStartingCloseTag := false
	var jsxInitializer jsxInitializer
	isJsxIdentifierExpected := false
	var importStatementCompletion *importStatementCompletionInfo
	location := astnav.GetTouchingPropertyName(file, position)
	keywordFilters := KeywordCompletionFiltersNone
	isNewIdentifierLocation := false
	// !!! flags := CompletionInfoFlagsNone
	var defaultCommitCharacters []string

	if contextToken != nil {
		importStatementCompletionInfo := l.getImportStatementCompletionInfo(contextToken, file)
		if importStatementCompletionInfo.keywordCompletion != ast.KindUnknown {
			if importStatementCompletionInfo.isKeywordOnlyCompletion {
				return &completionDataKeyword{
					keywordCompletions: []*CompletionItem{{
						CompletionItem: &lsproto.CompletionItem{
							Label:    scanner.TokenToString(importStatementCompletionInfo.keywordCompletion),
							Kind:     new(lsproto.CompletionItemKindKeyword),
							SortText: new(string(SortTextGlobalsOrKeywords)),
						},
					}},
					isNewIdentifierLocation: importStatementCompletionInfo.isNewIdentifierLocation,
				}, nil
			}
			keywordFilters = keywordFiltersFromSyntaxKind(importStatementCompletionInfo.keywordCompletion)
		}
		if importStatementCompletionInfo.replacementSpan != nil && preferences.IncludeCompletionsForImportStatements.IsTrue() {
			// !!! flags |= CompletionInfoFlags.IsImportStatementCompletion;
			importStatementCompletion = &importStatementCompletionInfo
			isNewIdentifierLocation = importStatementCompletionInfo.isNewIdentifierLocation
		}
		// Bail out if this is a known invalid completion location.
		if isCompletionListBlocker(contextToken, previousToken, location, file, position, typeChecker) {
			if keywordFilters != KeywordCompletionFiltersNone {
				isNewIdentifierLocation, _ := computeCommitCharactersAndIsNewIdentifier(contextToken, file, position)
				return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation), nil
			}
			return nil, nil
		}

		parent := contextToken.Parent
		if contextToken.Kind == ast.KindDotToken || contextToken.Kind == ast.KindQuestionDotToken {
			isRightOfDot = contextToken.Kind == ast.KindDotToken
			isRightOfQuestionDot = contextToken.Kind == ast.KindQuestionDotToken
			switch parent.Kind {
			case ast.KindPropertyAccessExpression:
				propertyAccessToConvert = parent
				node = propertyAccessToConvert.Expression()
				leftMostAccessExpression := ast.GetLeftmostAccessExpression(parent)
				if ast.NodeIsMissing(leftMostAccessExpression) ||
					((ast.IsCallExpression(node) || ast.IsFunctionLike(node)) &&
						node.End() == contextToken.Pos() &&
						lsutil.GetLastChild(node, file).Kind != ast.KindCloseParenToken) {
					// This is likely dot from incorrectly parsed expression and user is starting to write spread
					// eg: Math.min(./**/)
					// const x = function (./**/) {}
					// ({./**/})
					return nil, nil
				}
			case ast.KindQualifiedName:
				node = parent.AsQualifiedName().Left
			case ast.KindModuleDeclaration:
				node = parent.Name()
			case ast.KindImportType:
				node = parent
			case ast.KindMetaProperty:
				node = lsutil.GetFirstToken(parent, file)
				if node.Kind != ast.KindImportKeyword && node.Kind != ast.KindNewKeyword {
					panic("Unexpected token kind: " + node.Kind.String())
				}
			default:
				// There is nothing that precedes the dot, so this likely just a stray character
				// or leading into a '...' token. Just bail out instead.
				return nil, nil
			}
		} else { // !!! else if (!importStatementCompletion)
			// <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 &&
							astnav.FindChildOfKind(parent, ast.KindEqualsToken, file) != nil {
							jsxInitializer.initializer = previousToken
						}
					}
				}
			}
		}
	}

	completionKind := CompletionKindNone
	hasUnresolvedAutoImports := false
	// This also gets mutated in nested-functions after the return
	var symbols []*ast.Symbol
	var autoImports []*autoimport.FixAndExport
	// Keys are indexes of `symbols`.
	symbolToOriginInfoMap := map[int]*symbolOriginInfo{}
	symbolToSortTextMap := map[ast.SymbolId]SortText{}
	var seenPropertySymbols collections.Set[ast.SymbolId]
	isTypeOnlyLocation := insideJSDocTagTypeExpression || insideJsDocImportTag ||
		importStatementCompletion != nil && location.Parent != nil && ast.IsTypeOnlyImportOrExportDeclaration(location.Parent) ||
		!isContextTokenValueLocation(contextToken) &&
			(isPossiblyTypeArgumentPosition(contextToken, file, typeChecker) ||
				ast.IsPartOfTypeNode(location) ||
				isContextTokenTypeLocation(contextToken))

	addSymbolOriginInfo := func(symbol *ast.Symbol, insertQuestionDot bool, insertAwait bool) {
		symbolId := ast.GetSymbolId(symbol)
		if insertAwait && seenPropertySymbols.AddIfAbsent(symbolId) {
			symbolToOriginInfoMap[len(symbols)-1] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindPromise, insertQuestionDot)}
		} else if insertQuestionDot {
			symbolToOriginInfoMap[len(symbols)-1] = &symbolOriginInfo{kind: symbolOriginInfoKindNullable}
		}
	}

	addSymbolSortInfo := func(symbol *ast.Symbol) {
		symbolId := ast.GetSymbolId(symbol)
		if isStaticProperty(symbol) {
			symbolToSortTextMap[symbolId] = SortTextLocalDeclarationPriority
		}
	}

	addPropertySymbol := func(symbol *ast.Symbol, insertAwait bool, insertQuestionDot bool) {
		// For a computed property with an accessible name like `Symbol.iterator`,
		// we'll add a completion for the *name* `Symbol` instead of for the property.
		// If this is e.g. [Symbol.iterator], add a completion for `Symbol`.
		computedPropertyName := core.FirstNonNil(symbol.Declarations, func(decl *ast.Node) *ast.Node {
			name := ast.GetNameOfDeclaration(decl)
			if name != nil && name.Kind == ast.KindComputedPropertyName {
				return name
			}
			return nil
		})

		if computedPropertyName != nil {
			leftMostName := getLeftMostName(computedPropertyName.Expression()) // The completion is for `Symbol`, not `iterator`.
			var nameSymbol *ast.Symbol
			if leftMostName != nil {
				nameSymbol = typeChecker.GetSymbolAtLocation(leftMostName)
			}
			// If this is nested like for `namespace N { export const sym = Symbol(); }`, we'll add the completion for `N`.
			var firstAccessibleSymbol *ast.Symbol
			if nameSymbol != nil {
				firstAccessibleSymbol = getFirstSymbolInChain(nameSymbol, contextToken, typeChecker)
			}
			var firstAccessibleSymbolId ast.SymbolId
			if firstAccessibleSymbol != nil {
				firstAccessibleSymbolId = ast.GetSymbolId(firstAccessibleSymbol)
			}
			if firstAccessibleSymbolId != 0 && seenPropertySymbols.AddIfAbsent(firstAccessibleSymbolId) {
				symbols = append(symbols, firstAccessibleSymbol)
				symbolToSortTextMap[firstAccessibleSymbolId] = SortTextGlobalsOrKeywords
				moduleSymbol := firstAccessibleSymbol.Parent
				if moduleSymbol == nil ||
					!checker.IsExternalModuleSymbol(moduleSymbol) ||
					typeChecker.TryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.Name, moduleSymbol) != firstAccessibleSymbol {
					symbolToOriginInfoMap[len(symbols)-1] = &symbolOriginInfo{kind: getNullableSymbolOriginInfoKind(symbolOriginInfoKindSymbolMember, insertQuestionDot)}
				} else {
					// !!! auto-import symbol
				}
			} else if firstAccessibleSymbolId == 0 || !seenPropertySymbols.Has(firstAccessibleSymbolId) {
				symbols = append(symbols, symbol)
				addSymbolOriginInfo(symbol, insertQuestionDot, insertAwait)
				addSymbolSortInfo(symbol)
			}
		} else {
			symbols = append(symbols, symbol)
			addSymbolOriginInfo(symbol, insertQuestionDot, insertAwait)
			addSymbolSortInfo(symbol)
		}
	}

	addTypeProperties := func(t *checker.Type, insertAwait bool, insertQuestionDot bool) {
		if typeChecker.GetStringIndexType(t) != nil {
			isNewIdentifierLocation = true
			defaultCommitCharacters = []string{}
		}
		if isRightOfQuestionDot && len(typeChecker.GetCallSignatures(t)) != 0 {
			isNewIdentifierLocation = true
			if defaultCommitCharacters == nil {
				defaultCommitCharacters = slices.Clone(allCommitCharacters) // Only invalid commit character here would be `(`.
			}
		}

		var propertyAccess *ast.Node
		if node.Kind == ast.KindImportType {
			propertyAccess = node
		} else {
			propertyAccess = node.Parent
		}

		if inCheckedFile {
			for _, symbol := range typeChecker.GetApparentProperties(t) {
				if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, t, symbol) {
					addPropertySymbol(symbol, false /*insertAwait*/, insertQuestionDot)
				}
			}
		} else {
			// In javascript files, for union types, we don't just get the members that
			// the individual types have in common, we also include all the members that
			// each individual type has. This is because we're going to add all identifiers
			// anyways. So we might as well elevate the members that were at least part
			// of the individual types to a higher status since we know what they are.
			for _, symbol := range getPropertiesForCompletion(t, typeChecker) {
				if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, t, symbol) {
					symbols = append(symbols, symbol)
				}
			}
		}

		if insertAwait {
			promiseType := typeChecker.GetPromisedTypeOfPromise(t)
			if promiseType != nil {
				for _, symbol := range typeChecker.GetApparentProperties(promiseType) {
					if typeChecker.IsValidPropertyAccessForCompletions(propertyAccess, promiseType, symbol) {
						addPropertySymbol(symbol, true /*insertAwait*/, insertQuestionDot)
					}
				}
			}
		}
	}

	getTypeScriptMemberSymbols := func() {
		// Right of dot member completion list
		completionKind = CompletionKindPropertyAccess

		// Since this is qualified name check it's a type node location
		isImportType := ast.IsLiteralImportTypeNode(node)
		isTypeLocation := (isImportType && !node.AsImportTypeNode().IsTypeOf) ||
			ast.IsPartOfTypeNode(node.Parent) ||
			isPossiblyTypeArgumentPosition(contextToken, file, typeChecker)
		isRhsOfImportDeclaration := isInRightSideOfInternalImportEqualsDeclaration(node)
		if ast.IsEntityName(node) || isImportType || ast.IsPropertyAccessExpression(node) {
			isNamespaceName := ast.IsModuleDeclaration(node.Parent)
			if isNamespaceName {
				isNewIdentifierLocation = true
				defaultCommitCharacters = []string{}
			}
			symbol := typeChecker.GetSymbolAtLocation(node)
			if symbol != nil {
				symbol := checker.SkipAlias(symbol, typeChecker)
				if symbol.Flags&(ast.SymbolFlagsModule|ast.SymbolFlagsEnum) != 0 {
					var valueAccessNode *ast.Node
					if isImportType {
						valueAccessNode = node
					} else {
						valueAccessNode = node.Parent
					}
					// Extract module or enum members
					exportedSymbols := typeChecker.GetExportsOfModule(symbol)
					for _, exportedSymbol := range exportedSymbols {
						if exportedSymbol == nil {
							panic("getExporsOfModule() should all be defined")
						}
						isValidValueAccess := func(s *ast.Symbol) bool {
							return typeChecker.IsValidPropertyAccess(valueAccessNode, s.Name)
						}
						isValidTypeAccess := func(s *ast.Symbol) bool {
							return symbolCanBeReferencedAtTypeLocation(s, typeChecker, collections.Set[ast.SymbolId]{})
						}
						var isValidAccess bool
						if isNamespaceName {
							// At `namespace N.M/**/`, if this is the only declaration of `M`, don't include `M` as a completion.
							isValidAccess = exportedSymbol.Flags&ast.SymbolFlagsNamespace != 0 &&
								!core.Every(exportedSymbol.Declarations, func(declaration *ast.Declaration) bool {
									return declaration.Parent == node.Parent
								})
						} else if isRhsOfImportDeclaration {
							// Any kind is allowed when dotting off namespace in internal import equals declaration
							isValidAccess = isValidTypeAccess(exportedSymbol) || isValidValueAccess(exportedSymbol)
						} else if isTypeLocation || insideJSDocTagTypeExpression {
							isValidAccess = isValidTypeAccess(exportedSymbol)
						} else {
							isValidAccess = isValidValueAccess(exportedSymbol)
						}
						if isValidAccess {
							symbols = append(symbols, exportedSymbol)
						}
					}

					// If the module is merged with a value, we must get the type of the class and add its properties (for inherited static methods).
					if !isTypeLocation && !insideJSDocTagTypeExpression &&
						core.Some(
							symbol.Declarations,
							func(decl *ast.Declaration) bool {
								return decl.Kind != ast.KindSourceFile && decl.Kind != ast.KindModuleDeclaration && decl.Kind != ast.KindEnumDeclaration
							},
						) {
						t := typeChecker.GetNonOptionalType(typeChecker.GetTypeOfSymbolAtLocation(symbol, node))
						insertQuestionDot := false
						if typeChecker.IsNullableType(t) {
							canCorrectToQuestionDot := isRightOfDot && !isRightOfQuestionDot &&
								!preferences.IncludeAutomaticOptionalChainCompletions.IsFalse()
							if canCorrectToQuestionDot || isRightOfQuestionDot {
								t = typeChecker.GetNonNullableType(t)
								if canCorrectToQuestionDot {
									insertQuestionDot = true
								}
							}
						}
						addTypeProperties(t, node.Flags&ast.NodeFlagsAwaitContext != 0, insertQuestionDot)
					}

					return
				}
			}
		}

		if !isTypeLocation || checker.IsInTypeQuery(node) {
			// microsoft/TypeScript#39946. Pulling on the type of a node inside of a function with a contextual `this` parameter can result in a circularity
			// if the `node` is part of the exprssion of a `yield` or `return`. This circularity doesn't exist at compile time because
			// we will check (and cache) the type of `this` *before* checking the type of the node.
			typeChecker.TryGetThisTypeAtEx(node, false /*includeGlobalThis*/, nil)
			t := typeChecker.GetNonOptionalType(typeChecker.GetTypeAtLocation(node))

			if !isTypeLocation {
				insertQuestionDot := false
				if typeChecker.IsNullableType(t) {
					canCorrectToQuestionDot := isRightOfDot && !isRightOfQuestionDot &&
						!preferences.IncludeAutomaticOptionalChainCompletions.IsFalse()

					if canCorrectToQuestionDot || isRightOfQuestionDot {
						t = typeChecker.GetNonNullableType(t)
						if canCorrectToQuestionDot {
							insertQuestionDot = true
						}
					}
				}
				addTypeProperties(t, node.Flags&ast.NodeFlagsAwaitContext != 0, insertQuestionDot)
			} else {
				addTypeProperties(typeChecker.GetNonNullableType(t), false /*insertAwait*/, false /*insertQuestionDot*/)
			}
		}
	}

	// Aggregates relevant symbols for completion in object literals in type argument positions.
	tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols := func() (globalsSearch, error) {
		typeLiteralNode := tryGetTypeLiteralNode(contextToken)
		if typeLiteralNode == nil {
			return globalsSearchContinue, nil
		}

		intersectionTypeNode := core.IfElse(
			ast.IsIntersectionTypeNode(typeLiteralNode.Parent),
			typeLiteralNode.Parent,
			nil,
		)
		containerTypeNode := core.IfElse(
			intersectionTypeNode != nil,
			intersectionTypeNode,
			typeLiteralNode,
		)

		containerExpectedType := getConstraintOfTypeArgumentProperty(containerTypeNode, typeChecker)
		if containerExpectedType == nil {
			return globalsSearchContinue, nil
		}

		containerActualType := typeChecker.GetTypeFromTypeNode(containerTypeNode)

		members := getPropertiesForCompletion(containerExpectedType, typeChecker)
		existingMembers := getPropertiesForCompletion(containerActualType, typeChecker)

		existingMemberNames := collections.Set[string]{}
		for _, member := range existingMembers {
			existingMemberNames.Add(member.Name)
		}

		symbols = append(
			symbols,
			core.Filter(members, func(member *ast.Symbol) bool { return !existingMemberNames.Has(member.Name) })...,
		)

		completionKind = CompletionKindObjectPropertyDeclaration
		isNewIdentifierLocation = true

		return globalsSearchSuccess, nil
	}

	// Aggregates relevant symbols for completion in object literals and object binding patterns.
	// Relevant symbols are stored in the captured 'symbols' variable.
	tryGetObjectLikeCompletionSymbols := func() (globalsSearch, error) {
		if contextToken != nil && contextToken.Kind == ast.KindDotDotDotToken {
			return globalsSearchContinue, nil
		}
		objectLikeContainer := tryGetObjectLikeCompletionContainer(contextToken, position, file)
		if objectLikeContainer == nil {
			return globalsSearchContinue, nil
		}

		// We're looking up possible property names from contextual/inferred/declared type.
		completionKind = CompletionKindObjectPropertyDeclaration

		var typeMembers []*ast.Symbol
		var existingMembers []*ast.Declaration

		if objectLikeContainer.Kind == ast.KindObjectLiteralExpression {
			instantiatedType := tryGetObjectLiteralContextualType(objectLikeContainer, typeChecker)

			// Check completions for Object property value shorthand
			if instantiatedType == nil {
				if objectLikeContainer.Flags&ast.NodeFlagsInWithStatement != 0 {
					return globalsSearchFail, nil
				}
				return globalsSearchContinue, nil
			}
			completionsType := typeChecker.GetContextualType(objectLikeContainer, checker.ContextFlagsIgnoreNodeInferences)
			t := core.IfElse(completionsType != nil, completionsType, instantiatedType)
			stringIndexType := typeChecker.GetStringIndexType(t)
			numberIndexType := typeChecker.GetNumberIndexType(t)
			isNewIdentifierLocation = stringIndexType != nil || numberIndexType != nil
			typeMembers = getPropertiesForObjectExpression(instantiatedType, completionsType, objectLikeContainer, typeChecker)
			existingMembers = objectLikeContainer.Properties()

			if len(typeMembers) == 0 {
				// Edge case: If NumberIndexType exists
				if numberIndexType == nil {
					return globalsSearchContinue, nil
				}
			}
		} else {
			if objectLikeContainer.Kind != ast.KindObjectBindingPattern {
				panic("Expected 'objectLikeContainer' to be an object binding pattern.")
			}
			// We are *only* completing on properties from the type being destructured.
			isNewIdentifierLocation = false
			rootDeclaration := ast.GetRootDeclaration(objectLikeContainer.Parent)
			if !ast.IsVariableLike(rootDeclaration) {
				panic("Root declaration is not variable-like.")
			}

			// We don't want to complete using the type acquired by the shape
			// of the binding pattern; we are only interested in types acquired
			// through type declaration or inference.
			// Also proceed if rootDeclaration is a parameter and if its containing function expression/arrow function is contextually typed -
			// type of parameter will flow in from the contextual type of the function.
			canGetType := ast.HasInitializer(rootDeclaration) ||
				ast.GetTypeAnnotationNode(rootDeclaration) != nil ||
				rootDeclaration.Parent.Parent.Kind == ast.KindForOfStatement
			if !canGetType && rootDeclaration.Kind == ast.KindParameter {
				if ast.IsExpression(rootDeclaration.Parent) {
					canGetType = typeChecker.GetContextualType(rootDeclaration.Parent, checker.ContextFlagsNone) != nil
				} else if rootDeclaration.Parent.Kind == ast.KindMethodDeclaration ||
					rootDeclaration.Parent.Kind == ast.KindSetAccessor {
					canGetType = ast.IsExpression(rootDeclaration.Parent.Parent) &&
						typeChecker.GetContextualType(rootDeclaration.Parent.Parent, checker.ContextFlagsNone) != nil
				}
			}
			if canGetType {
				typeForObject := typeChecker.GetTypeAtLocation(objectLikeContainer)
				if typeForObject == nil {
					return globalsSearchFail, nil
				}
				typeMembers = core.Filter(
					typeChecker.GetPropertiesOfType(typeForObject),
					func(propertySymbol *ast.Symbol) bool {
						return typeChecker.IsPropertyAccessible(
							objectLikeContainer,
							false, /*isSuper*/
							false, /*isWrite*/
							typeForObject,
							propertySymbol,
						)
					},
				)
				existingMembers = objectLikeContainer.Elements()
			}
		}

		if len(typeMembers) > 0 {
			// Add filtered items to the completion list.
			filteredMembers, spreadMemberNames := filterObjectMembersList(
				typeMembers,
				core.CheckEachDefined(existingMembers, "object like properties or elements should all be defined"),
				file,
				position,
				typeChecker,
			)
			symbols = append(symbols, filteredMembers...)

			// Set sort texts.
			transformObjectLiteralMembers := preferences.IncludeCompletionsWithObjectLiteralMethodSnippets.IsTrue() &&
				objectLikeContainer.Kind == ast.KindObjectLiteralExpression
			for _, member := range filteredMembers {
				symbolId := ast.GetSymbolId(member)
				if spreadMemberNames.Has(member.Name) {
					symbolToSortTextMap[symbolId] = SortTextMemberDeclaredBySpreadAssignment
				}
				if member.Flags&ast.SymbolFlagsOptional != 0 {
					_, ok := symbolToSortTextMap[symbolId]
					if !ok {
						symbolToSortTextMap[symbolId] = SortTextOptionalMember
					}
				}
				if transformObjectLiteralMembers {
					// !!! object literal member snippet completions
				}
			}
		}

		return globalsSearchSuccess, nil
	}

	shouldOfferImportCompletions := func() bool {
		if tspath.IsDynamicFileName(file.FileName()) {
			return false
		}
		// If already typing an import statement, provide completions for it.
		if importStatementCompletion != nil {
			return true
		}
		// If not already a module, must have modules enabled.
		if preferences.IncludeCompletionsForModuleExports.IsFalse() {
			return false
		}
		// Always using ES modules in 6.0+
		return true
	}

	// Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap`
	collectAutoImports := func() error {
		// `completionItem/resolve` for auto-import completions should be resolved via the completion item data,
		// so we don't need to collect auto-import entries again.
		if forItemResolve {
			return nil
		}
		if !shouldOfferImportCompletions() {
			return nil
		}

		// import { type | -> token text should be blank
		var lowerCaseTokenText string
		usagePosition := l.createLspPosition(position, file)
		if previousToken != nil && ast.IsIdentifier(previousToken) {
			usagePosition = l.createLspPosition(scanner.GetTokenPosOfNode(previousToken, file, false /*includeJSDoc*/), file)
			if !(previousToken == contextToken && importStatementCompletion != nil) {
				lowerCaseTokenText = strings.ToLower(previousToken.Text())
			}
		}

		view, err := l.getPreparedAutoImportView(file)
		if err != nil {
			return err
		}
		if view == nil {
			return nil
		}

		autoImports = view.GetCompletions(ctx, lowerCaseTokenText, usagePosition, isRightOfOpenTag, isTypeOnlyLocation)
		return nil
	}

	tryGetImportCompletionSymbols := func() (globalsSearch, error) {
		if importStatementCompletion == nil {
			return globalsSearchContinue, nil
		}
		isNewIdentifierLocation = true
		if err := collectAutoImports(); err != nil {
			return globalsSearchFail, err
		}
		return globalsSearchSuccess, nil
	}

	// Aggregates relevant symbols for completion in import clauses and export clauses
	// whose declarations have a module specifier; for instance, symbols will be aggregated for
	//
	//      import { | } from "moduleName";
	//      export { a as foo, | } from "moduleName";
	//
	// but not for
	//
	//      export { | };
	//
	// Relevant symbols are stored in the captured 'symbols' variable.
	tryGetImportOrExportClauseCompletionSymbols := func() (globalsSearch, error) {
		if contextToken == nil {
			return globalsSearchContinue, nil
		}

		// `import { |` or `import { a as 0, | }` or `import { type | }`
		var namedImportsOrExports *ast.NamedImportsOrExports
		if contextToken.Kind == ast.KindOpenBraceToken || contextToken.Kind == ast.KindCommaToken {
			namedImportsOrExports = core.IfElse(isNamedImportsOrExports(contextToken.Parent), contextToken.Parent, nil)
		} else if isTypeKeywordTokenOrIdentifier(contextToken) {
			namedImportsOrExports = core.IfElse(
				isNamedImportsOrExports(contextToken.Parent.Parent),
				contextToken.Parent.Parent,
				nil,
			)
		}

		if namedImportsOrExports == nil {
			return globalsSearchContinue, nil
		}

		// We can at least offer `type` at `import { |`
		if !isTypeKeywordTokenOrIdentifier(contextToken) {
			keywordFilters = KeywordCompletionFiltersTypeKeyword
		}

		// try to show exported member for imported/re-exported module
		moduleSpecifier := core.IfElse(
			namedImportsOrExports.Kind == ast.KindNamedImports,
			namedImportsOrExports.Parent.Parent,
			namedImportsOrExports.Parent,
		).ModuleSpecifier()
		if moduleSpecifier == nil {
			isNewIdentifierLocation = true
			if namedImportsOrExports.Kind == ast.KindNamedImports {
				return globalsSearchFail, nil
			}
			return globalsSearchContinue, nil
		}

		moduleSpecifierSymbol := typeChecker.GetSymbolAtLocation(moduleSpecifier)
		if moduleSpecifierSymbol == nil {
			isNewIdentifierLocation = true
			return globalsSearchFail, nil
		}

		completionKind = CompletionKindMemberLike
		isNewIdentifierLocation = false
		exports := typeChecker.GetExportsAndPropertiesOfModule(moduleSpecifierSymbol)

		existing := collections.Set[string]{}
		for _, element := range namedImportsOrExports.Elements() {
			if isCurrentlyEditingNode(element, file, position) {
				continue
			}
			existing.Add(element.PropertyNameOrName().Text())
		}
		uniques := core.Filter(exports, func(symbol *ast.Symbol) bool {
			return ast.SymbolName(symbol) != ast.InternalSymbolNameDefault && !existing.Has(ast.SymbolName(symbol))
		})

		symbols = append(symbols, uniques...)
		if len(uniques) == 0 {
			// If there's nothing else to import, don't offer `type` either.
			keywordFilters = KeywordCompletionFiltersNone
		}
		return globalsSearchSuccess, nil
	}

	// import { x } from "foo" with { | }
	tryGetImportAttributesCompletionSymbols := func() (globalsSearch, error) {
		if contextToken == nil {
			return globalsSearchContinue, nil
		}

		var importAttributes *ast.Node
		switch contextToken.Kind {
		case ast.KindOpenBraceToken, ast.KindCommaToken:
			importAttributes = contextToken.Parent
		case ast.KindColonToken:
			importAttributes = contextToken.Parent.Parent
		}
		if importAttributes == nil || !ast.IsImportAttributes(importAttributes) {
			return globalsSearchContinue, nil
		}

		var elements []*ast.Node
		if importAttributes.AsImportAttributes().Attributes != nil {
			elements = importAttributes.AsImportAttributes().Attributes.Nodes
		}
		attributeNames := core.Map(elements, func(el *ast.Node) string {
			return el.AsImportAttribute().Name().Text()
		})
		existing := collections.NewSetFromItems(attributeNames...)
		uniques := core.Filter(
			typeChecker.GetApparentProperties(typeChecker.GetTypeAtLocation(importAttributes)),
			func(symbol *ast.Symbol) bool {
				return !existing.Has(ast.SymbolName(symbol))
			},
		)
		symbols = append(symbols, uniques...)
		return globalsSearchSuccess, nil
	}

	// Adds local declarations for completions in named exports:
	//   export { | };
	// Does not check for the absence of a module specifier (`export {} from "./other"`)
	// because `tryGetImportOrExportClauseCompletionSymbols` runs first and handles that,
	// preventing this function from running.
	tryGetLocalNamedExportCompletionSymbols := func() (globalsSearch, error) {
		if contextToken == nil {
			return globalsSearchContinue, nil
		}
		var namedExports *ast.NamedExportsNode
		if contextToken.Kind == ast.KindOpenBraceToken || contextToken.Kind == ast.KindCommaToken {
			namedExports = core.IfElse(ast.IsNamedExports(contextToken.Parent), contextToken.Parent, nil)
		}

		if namedExports == nil {
			return globalsSearchContinue, nil
		}

		localsContainer := ast.FindAncestor(namedExports, func(node *ast.Node) bool {
			return ast.IsSourceFile(node) || ast.IsModuleDeclaration(node)
		})
		completionKind = CompletionKindNone
		isNewIdentifierLocation = false
		localSymbol := localsContainer.Symbol()
		var localExports ast.SymbolTable
		if localSymbol != nil {
			localExports = localSymbol.Exports
		}
		for name, symbol := range localsContainer.Locals() {
			symbols = append(symbols, symbol)
			if _, ok := localExports[name]; ok {
				symbolId := ast.GetSymbolId(symbol)
				symbolToSortTextMap[symbolId] = SortTextOptionalMember
			}
		}

		return globalsSearchSuccess, nil
	}

	tryGetConstructorCompletion := func() (globalsSearch, error) {
		if tryGetConstructorLikeCompletionContainer(contextToken) == nil {
			return globalsSearchContinue, nil
		}

		// no members, only keywords
		completionKind = CompletionKindNone
		// Declaring new property/method/accessor
		isNewIdentifierLocation = true
		// Has keywords for constructor parameter
		keywordFilters = KeywordCompletionFiltersConstructorParameterKeywords
		return globalsSearchSuccess, nil
	}

	// Aggregates relevant symbols for completion in class declaration
	// Relevant symbols are stored in the captured 'symbols' variable.
	tryGetClassLikeCompletionSymbols := func() (globalsSearch, error) {
		decl := tryGetObjectTypeDeclarationCompletionContainer(file, contextToken, location, position)
		if decl == nil {
			return globalsSearchContinue, nil
		}

		// We're looking up possible property names from parent type.
		completionKind = CompletionKindMemberLike
		// Declaring new property/method/accessor
		isNewIdentifierLocation = true
		if contextToken.Kind == ast.KindAsteriskToken {
			keywordFilters = KeywordCompletionFiltersNone
		} else if ast.IsClassLike(decl) {
			keywordFilters = KeywordCompletionFiltersClassElementKeywords
		} else {
			keywordFilters = KeywordCompletionFiltersInterfaceElementKeywords
		}

		// If you're in an interface you don't want to repeat things from super-interface. So just stop here.
		if !ast.IsClassLike(decl) {
			return globalsSearchSuccess, nil
		}

		var classElement *ast.Node
		if contextToken.Kind == ast.KindSemicolonToken {
			classElement = contextToken.Parent.Parent
		} else {
			classElement = contextToken.Parent
		}
		var classElementModifierFlags ast.ModifierFlags
		if ast.IsClassElement(classElement) {
			classElementModifierFlags = classElement.ModifierFlags()
		}
		// If this is context token is not something we are editing now, consider if this would lead to be modifier.
		if contextToken.Kind == ast.KindIdentifier && !isCurrentlyEditingNode(contextToken, file, position) {
			switch contextToken.Text() {
			case "private":
				classElementModifierFlags |= ast.ModifierFlagsPrivate
			case "static":
				classElementModifierFlags |= ast.ModifierFlagsStatic
			case "override":
				classElementModifierFlags |= ast.ModifierFlagsOverride
			}
		}
		if ast.IsClassStaticBlockDeclaration(classElement) {
			classElementModifierFlags |= ast.ModifierFlagsStatic
		}

		// No member list for private methods
		if classElementModifierFlags&ast.ModifierFlagsPrivate == 0 {
			// List of property symbols of base type that are not private and already implemented
			var baseTypeNodes []*ast.Node
			if ast.IsClassLike(decl) && classElementModifierFlags&ast.ModifierFlagsOverride != 0 {
				baseTypeNodes = core.SingleElementSlice(ast.GetClassExtendsHeritageElement(decl))
			} else {
				baseTypeNodes = getAllSuperTypeNodes(decl)
			}
			var baseSymbols []*ast.Symbol
			for _, baseTypeNode := range baseTypeNodes {
				t := typeChecker.GetTypeAtLocation(baseTypeNode)
				if classElementModifierFlags&ast.ModifierFlagsStatic != 0 {
					if t.Symbol() != nil {
						baseSymbols = append(
							baseSymbols,
							typeChecker.GetPropertiesOfType(typeChecker.GetTypeOfSymbolAtLocation(t.Symbol(), decl))...,
						)
					}
				} else if t != nil {
					baseSymbols = append(baseSymbols, typeChecker.GetPropertiesOfType(t)...)
				}
			}

			symbols = append(symbols,
				filterClassMembersList(baseSymbols, decl.Members(), classElementModifierFlags, file, position)...)
			for index, symbol := range symbols {
				declaration := symbol.ValueDeclaration
				if declaration != nil && ast.IsClassElement(declaration) &&
					declaration.Name() != nil &&
					ast.IsComputedPropertyName(declaration.Name()) {
					origin := &symbolOriginInfo{
						kind: symbolOriginInfoKindComputedPropertyName,
						data: &symbolOriginInfoComputedPropertyName{symbolName: typeChecker.SymbolToString(symbol)},
					}
					symbolToOriginInfoMap[index] = origin
				}
			}
		}

		return globalsSearchSuccess, nil
	}

	tryGetJsxCompletionSymbols := func() (globalsSearch, error) {
		jsxContainer := tryGetContainingJsxElement(contextToken, file)
		if jsxContainer == nil {
			return globalsSearchContinue, nil
		}
		// Cursor is inside a JSX self-closing element or opening element.
		attrsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsNone)
		if attrsType == nil {
			return globalsSearchContinue, nil
		}
		completionsType := typeChecker.GetContextualType(jsxContainer.Attributes(), checker.ContextFlagsIgnoreNodeInferences)
		filteredSymbols, spreadMemberNames := filterJsxAttributes(
			getPropertiesForObjectExpression(attrsType, completionsType, jsxContainer.Attributes(), typeChecker),
			jsxContainer.Attributes().Properties(),
			file,
			position,
			typeChecker,
		)

		symbols = append(symbols, filteredSymbols...)
		// Set sort texts.
		for _, symbol := range filteredSymbols {
			symbolId := ast.GetSymbolId(symbol)
			if spreadMemberNames.Has(ast.SymbolName(symbol)) {
				symbolToSortTextMap[symbolId] = SortTextMemberDeclaredBySpreadAssignment
			}
			if symbol.Flags&ast.SymbolFlagsOptional != 0 {
				_, ok := symbolToSortTextMap[symbolId]
				if !ok {
					symbolToSortTextMap[symbolId] = SortTextOptionalMember
				}
			}
		}

		completionKind = CompletionKindMemberLike
		isNewIdentifierLocation = false
		return globalsSearchSuccess, nil
	}

	getGlobalCompletions := func() (globalsSearch, error) {
		if tryGetFunctionLikeBodyCompletionContainer(contextToken) != nil {
			keywordFilters = KeywordCompletionFiltersFunctionLikeBodyKeywords
		} else {
			keywordFilters = KeywordCompletionFiltersAll
		}
		// Get all entities in the current scope.
		completionKind = CompletionKindGlobal
		isNewIdentifierLocation, defaultCommitCharacters = computeCommitCharactersAndIsNewIdentifier(contextToken, file, position)

		if previousToken != contextToken {
			if previousToken == nil {
				panic("Expected 'contextToken' to be defined when different from 'previousToken'.")
			}
		}

		// We need to find the node that will give us an appropriate scope to begin
		// aggregating completion candidates. This is achieved in 'getScopeNode'
		// by finding the first node that encompasses a position, accounting for whether a node
		// is "complete" to decide whether a position belongs to the node.
		//
		// However, at the end of an identifier, we are interested in the scope of the identifier
		// itself, but fall outside of the identifier. For instance:
		//
		//      xyz => x$
		//
		// the cursor is outside of both the 'x' and the arrow function 'xyz => x',
		// so 'xyz' is not returned in our results.
		//
		// We define 'adjustedPosition' so that we may appropriately account for
		// being at the end of an identifier. The intention is that if requesting completion
		// at the end of an identifier, it should be effectively equivalent to requesting completion
		// anywhere inside/at the beginning of the identifier. So in the previous case, the
		// 'adjustedPosition' will work as if requesting completion in the following:
		//
		//      xyz => $x
		//
		// If previousToken !== contextToken, then
		//   - 'contextToken' was adjusted to the token prior to 'previousToken'
		//      because we were at the end of an identifier.
		//   - 'previousToken' is defined.
		var adjustedPosition int
		if previousToken != contextToken {
			adjustedPosition = astnav.GetStartOfNode(previousToken, file, false /*includeJSDoc*/)
		} else {
			adjustedPosition = position
		}

		scopeNode := getScopeNode(contextToken, adjustedPosition, file)
		if scopeNode == nil {
			scopeNode = file.AsNode()
		}
		isInSnippetScope = isSnippetScope(scopeNode)

		symbolMeanings := core.IfElse(isTypeOnlyLocation, ast.SymbolFlagsNone, ast.SymbolFlagsValue) |
			ast.SymbolFlagsType | ast.SymbolFlagsNamespace | ast.SymbolFlagsAlias
		typeOnlyAliasNeedsPromotion := previousToken != nil && !ast.IsValidTypeOnlyAliasUseSite(previousToken)

		symbols = append(symbols, typeChecker.GetSymbolsInScope(scopeNode, symbolMeanings)...)
		core.CheckEachDefined(symbols, "getSymbolsInScope() should all be defined")
		for index, symbol := range symbols {
			symbolId := ast.GetSymbolId(symbol)
			if !typeChecker.IsArgumentsSymbol(symbol) &&
				!core.Some(symbol.Declarations, func(decl *ast.Declaration) bool {
					return ast.GetSourceFileOfNode(decl) == file
				}) {
				symbolToSortTextMap[symbolId] = SortTextGlobalsOrKeywords
			}
			if typeOnlyAliasNeedsPromotion && symbol.Flags&ast.SymbolFlagsValue == 0 {
				typeOnlyAliasDeclaration := core.Find(symbol.Declarations, ast.IsTypeOnlyImportDeclaration)
				if typeOnlyAliasDeclaration != nil {
					origin := &symbolOriginInfo{
						kind: symbolOriginInfoKindTypeOnlyAlias,
						data: &symbolOriginInfoTypeOnlyAlias{declaration: typeOnlyAliasDeclaration},
					}
					symbolToOriginInfoMap[index] = origin
				}
			}
		}

		// Need to insert 'this.' before properties of `this` type.
		if scopeNode.Kind != ast.KindSourceFile {
			thisType := typeChecker.TryGetThisTypeAtEx(
				scopeNode,
				false, /*includeGlobalThis*/
				core.IfElse(ast.IsClassLike(scopeNode.Parent), scopeNode, nil),
			)
			if thisType != nil && !isProbablyGlobalType(thisType, file, typeChecker) {
				for _, symbol := range getPropertiesForCompletion(thisType, typeChecker) {
					symbolId := ast.GetSymbolId(symbol)
					symbols = append(symbols, symbol)
					symbolToOriginInfoMap[len(symbols)-1] = &symbolOriginInfo{kind: symbolOriginInfoKindThisType}
					symbolToSortTextMap[symbolId] = SortTextSuggestedClassMembers
				}
			}
		}

		if err := collectAutoImports(); err != nil {
			return globalsSearchFail, err
		}
		if isTypeOnlyLocation {
			if contextToken != nil && ast.IsAssertionExpression(contextToken.Parent) {
				keywordFilters = KeywordCompletionFiltersTypeAssertionKeywords
			} else {
				keywordFilters = KeywordCompletionFiltersTypeKeywords
			}
		}

		return globalsSearchSuccess, nil
	}

	tryGetGlobalSymbols := func() (bool, error) {
		var result globalsSearch
		var err error
		globalSearchFuncs := []func() (globalsSearch, error){
			tryGetObjectTypeLiteralInTypeArgumentCompletionSymbols,
			tryGetObjectLikeCompletionSymbols,
			tryGetImportCompletionSymbols,
			tryGetImportOrExportClauseCompletionSymbols,
			tryGetImportAttributesCompletionSymbols,
			tryGetLocalNamedExportCompletionSymbols,
			tryGetConstructorCompletion,
			tryGetClassLikeCompletionSymbols,
			tryGetJsxCompletionSymbols,
			getGlobalCompletions,
		}
		for _, globalSearchFunc := range globalSearchFuncs {
			result, err = globalSearchFunc()
			if err != nil {
				return false, err
			}
			if result != globalsSearchContinue {
				break
			}
		}
		return result == globalsSearchSuccess, nil
	}

	if isRightOfDot || isRightOfQuestionDot {
		getTypeScriptMemberSymbols()
	} else if isRightOfOpenTag {
		symbols = typeChecker.GetJsxIntrinsicTagNamesAt(location)
		core.CheckEachDefined(symbols, "GetJsxIntrinsicTagNamesAt() should all be defined")
		if _, err := tryGetGlobalSymbols(); err != nil {
			return nil, err
		}
		completionKind = CompletionKindGlobal
		keywordFilters = KeywordCompletionFiltersNone
	} else if isStartingCloseTag {
		tagName := contextToken.Parent.Parent.AsJsxElement().OpeningElement.TagName()
		tagSymbol := typeChecker.GetSymbolAtLocation(tagName)
		if tagSymbol != nil {
			symbols = []*ast.Symbol{tagSymbol}
		}
		completionKind = CompletionKindGlobal
		keywordFilters = KeywordCompletionFiltersNone
	} else {
		// For JavaScript or TypeScript, if we're not after a dot, then just try to get the
		// global symbols in scope.  These results should be valid for either language as
		// the set of symbols that can be referenced from this location.
		if ok, err := tryGetGlobalSymbols(); !ok {
			if err != nil {
				return nil, err
			}
			if keywordFilters != KeywordCompletionFiltersNone {
				return keywordCompletionData(keywordFilters, isJSOnlyLocation, isNewIdentifierLocation), nil
			}
			return nil, nil
		}
	}

	var contextualTypeOrConstraint *checker.Type
	if previousToken != nil {
		contextualTypeOrConstraint = getContextualType(previousToken, position, file, typeChecker)
		if contextualTypeOrConstraint == nil {
			contextualTypeOrConstraint = getConstraintOfTypeArgumentProperty(previousToken, typeChecker)
		}
	}

	// exclude literal suggestions after <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 contextualTypeOrConstraint != nil && contextualTypeOrConstraint.IsUnion() {
			types = contextualTypeOrConstraint.Types()
		} else if contextualTypeOrConstraint != nil {
			types = []*checker.Type{contextualTypeOrConstraint}
		}
		literals = core.MapNonNil(types, func(t *checker.Type) literalValue {
			if isLiteral(t) && !t.IsEnumLiteral() {
				return t.AsLiteralType().Value()
			}
			return nil
		})
	}

	var recommendedCompletion *ast.Symbol
	if previousToken != nil && contextualTypeOrConstraint != nil {
		recommendedCompletion = getRecommendedCompletion(previousToken, contextualTypeOrConstraint, typeChecker)
	}

	if defaultCommitCharacters == nil {
		defaultCommitCharacters = getDefaultCommitCharacters(isNewIdentifierLocation)
	}

	return &completionDataData{
		symbols:                      symbols,
		autoImports:                  autoImports,
		completionKind:               completionKind,
		isInSnippetScope:             isInSnippetScope,
		propertyAccessToConvert:      propertyAccessToConvert,
		isNewIdentifierLocation:      isNewIdentifierLocation,
		location:                     location,
		keywordFilters:               keywordFilters,
		literals:                     literals,
		symbolToOriginInfoMap:        symbolToOriginInfoMap,
		symbolToSortTextMap:          symbolToSortTextMap,
		recommendedCompletion:        recommendedCompletion,
		previousToken:                previousToken,
		contextToken:                 contextToken,
		jsxInitializer:               jsxInitializer,
		insideJSDocTagTypeExpression: insideJSDocTagTypeExpression,
		isTypeOnlyLocation:           isTypeOnlyLocation,
		isJsxIdentifierExpected:      isJsxIdentifierExpected,
		isRightOfOpenTag:             isRightOfOpenTag,
		isRightOfDotOrQuestionDot:    isRightOfDot || isRightOfQuestionDot,
		importStatementCompletion:    importStatementCompletion,
		hasUnresolvedAutoImports:     hasUnresolvedAutoImports,
		defaultCommitCharacters:      defaultCommitCharacters,
	}, nil
}

func keywordCompletionData(
	keywordFilters KeywordCompletionFilters,
	filterOutTSOnlyKeywords bool,
	isNewIdentifierLocation bool,
) *completionDataKeyword {
	return &completionDataKeyword{
		keywordCompletions:      getKeywordCompletions(keywordFilters, filterOutTSOnlyKeywords),
		isNewIdentifierLocation: isNewIdentifierLocation,
	}
}

func getDefaultCommitCharacters(isNewIdentifierLocation bool) []string {
	if isNewIdentifierLocation {
		return []string{}
	}
	return slices.Clone(allCommitCharacters)
}

func (l *LanguageService) completionInfoFromData(
	ctx context.Context,
	typeChecker *checker.Checker,
	file *ast.SourceFile,
	compilerOptions *core.CompilerOptions,
	data *completionDataData,
	position int,
	optionalReplacementSpan *lsproto.Range,
	includeSymbols bool,
) (*CompletionList, error) {
	keywordFilters := data.keywordFilters
	isNewIdentifierLocation := data.isNewIdentifierLocation
	contextToken := data.contextToken
	literals := data.literals
	preferences := l.UserPreferences()

	// Verify if the file is JSX language variant
	if file.LanguageVariant == core.LanguageVariantJSX {
		list := l.getJsxClosingTagCompletion(ctx, data.location, file, position)
		if list != nil {
			return list, nil
		}
	}

	// When the completion is for the expression of a case clause (e.g. `case |`),
	// filter literals & enum symbols whose values are already present in existing case clauses.
	caseClause := ast.FindAncestor(contextToken, ast.IsCaseClause)
	if caseClause != nil &&
		(contextToken.Kind == ast.KindCaseKeyword ||
			ast.IsNodeDescendantOf(contextToken, caseClause.Expression())) {
		tracker := newCaseClauseTracker(typeChecker, caseClause.Parent.AsCaseBlock().Clauses.Nodes)
		literals = core.Filter(literals, func(literal literalValue) bool {
			return !tracker.hasValue(literal)
		})
		data.symbols = core.Filter(data.symbols, func(symbol *ast.Symbol) bool {
			if symbol.ValueDeclaration != nil && ast.IsEnumMember(symbol.ValueDeclaration) {
				value := typeChecker.GetConstantValue(symbol.ValueDeclaration)
				if value != nil && tracker.hasValue(value) {
					return false
				}
			}
			return true
		})
	}

	isChecked := isCheckedFile(file, compilerOptions)
	if isChecked && !isNewIdentifierLocation && len(data.symbols) == 0 && keywordFilters == KeywordCompletionFiltersNone {
		return nil, nil
	}

	uniqueNames, sortedEntries := l.getCompletionEntriesFromSymbols(
		ctx,
		typeChecker,
		data,
		nil, /*replacementToken*/
		position,
		file,
		compilerOptions,
		includeSymbols,
	)

	if data.keywordFilters != KeywordCompletionFiltersNone {
		keywordCompletions := getKeywordCompletions(data.keywordFilters, !data.insideJSDocTagTypeExpression && ast.IsSourceFileJS(file))
		for _, keywordEntry := range keywordCompletions {
			if data.isTypeOnlyLocation && isTypeKeyword(scanner.StringToToken(keywordEntry.Label)) ||
				!data.isTypeOnlyLocation && isContextualKeywordInAutoImportableExpressionSpace(keywordEntry.Label) ||
				!uniqueNames.Has(keywordEntry.Label) {
				uniqueNames.Add(keywordEntry.Label)
				sortedEntries = append(sortedEntries, keywordEntry)
			}
		}
	}

	for _, keywordEntry := range getContextualKeywords(file, contextToken, position) {
		if !uniqueNames.Has(keywordEntry.Label) {
			uniqueNames.Add(keywordEntry.Label)
			sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: keywordEntry})
		}
	}

	for _, literal := range literals {
		literalEntry := createCompletionItemForLiteral(file, preferences, literal)
		uniqueNames.Add(literalEntry.Label)
		sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: literalEntry})
	}

	if !isChecked {
		sortedEntries = l.getJSCompletionEntries(
			ctx,
			file,
			position,
			&uniqueNames,
			sortedEntries,
		)
	}

	if contextToken != nil && !data.isRightOfOpenTag && !data.isRightOfDotOrQuestionDot {
		if caseBlock := ast.FindAncestorKind(contextToken, ast.KindCaseBlock); caseBlock != nil {
			casesItem, err := l.getExhaustiveCaseSnippets(
				ctx,
				caseBlock.AsCaseBlock(),
				file,
				position,
				compilerOptions,
				l.program,
				typeChecker,
			)
			if err != nil {
				return nil, err
			}
			if casesItem != nil {
				sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: casesItem})
			}
		}
	}

	itemDefaults := l.setItemDefaults(
		ctx,
		position,
		file,
		sortedEntries,
		&data.defaultCommitCharacters,
		optionalReplacementSpan,
	)

	return &CompletionList{
		IsIncomplete: data.hasUnresolvedAutoImports,
		ItemDefaults: itemDefaults,
		Items:        sortedEntries,
	}, nil
}

func (l *LanguageService) getCompletionEntriesFromSymbols(
	ctx context.Context,
	typeChecker *checker.Checker,
	data *completionDataData,
	replacementToken *ast.Node,
	position int,
	file *ast.SourceFile,
	compilerOptions *core.CompilerOptions,
	includeSymbols bool,
) (uniqueNames collections.Set[string], sortedEntries []*CompletionItem) {
	closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location)
	useSemicolons := lsutil.ProbablyUsesSemicolons(file)
	isMemberCompletion := isMemberCompletionKind(data.completionKind)
	sortedEntries = slices.Grow(sortedEntries, len(data.symbols)+len(data.autoImports))
	// Tracks unique names.
	// Value is set to false for global variables or completions from external module exports, because we can have multiple of those;
	// true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports.
	// So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name.
	uniques := make(uniqueNamesMap)
	for index, symbol := range data.symbols {
		origin := data.symbolToOriginInfoMap[index]
		name, needsConvertPropertyAccess := getCompletionEntryDisplayNameForSymbol(
			symbol,
			origin,
			data.completionKind,
			data.isJsxIdentifierExpected,
		)
		if name == "" ||
			uniques[name] && (origin == nil || !originIsObjectLiteralMethod(origin)) ||
			data.completionKind == CompletionKindGlobal &&
				!shouldIncludeSymbol(symbol, data, closestSymbolDeclaration, file, typeChecker, compilerOptions) {
			continue
		}

		// When in a value location in a JS file, ignore symbols that definitely seem to be type-only.
		if !data.isTypeOnlyLocation && ast.IsSourceFileJS(file) && symbolAppearsToBeTypeOnly(symbol, typeChecker) {
			continue
		}

		originalSortText := data.symbolToSortTextMap[ast.GetSymbolId(symbol)]
		if originalSortText == "" {
			originalSortText = SortTextLocationPriority
		}

		var sortText SortText
		if isDeprecated(symbol, typeChecker) {
			sortText = DeprecateSortText(originalSortText)
		} else {
			sortText = originalSortText
		}
		entry := l.createCompletionItem(
			ctx,
			typeChecker,
			symbol,
			sortText,
			replacementToken,
			data,
			position,
			file,
			name,
			needsConvertPropertyAccess,
			origin,
			useSemicolons,
			compilerOptions,
			isMemberCompletion,
		)
		if entry == nil {
			continue
		}

		// True for locals; false for globals, module exports from other files, `this.` completions.
		shouldShadowLaterSymbols := (origin == nil || originIsTypeOnlyAlias(origin)) &&
			!(symbol.Parent == nil &&
				!core.Some(symbol.Declarations, func(d *ast.Node) bool { return ast.GetSourceFileOfNode(d) == file }))
		uniques[name] = shouldShadowLaterSymbols
		var sym *ast.Symbol
		if includeSymbols {
			sym = symbol
		}
		sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: entry, Symbol: sym})
	}

	for _, autoImport := range data.autoImports {
		// !!! check for type-only in JS
		// !!! deprecation

		if data.importStatementCompletion != nil {
			// !!!
			continue
		}

		// Non-contextual keywords (e.g., `function`, `class`, `const`) cannot be used as identifiers,
		// so auto-imports with these names should not shadow keyword completions.
		if token := scanner.StringToToken(autoImport.Fix.Name); token != ast.KindUnknown && ast.IsNonContextualKeyword(token) {
			continue
		}

		if !autoImport.Export.IsUnresolvedAlias() {
			if data.isTypeOnlyLocation {
				if autoImport.Export.Flags&ast.SymbolFlagsType == 0 && autoImport.Export.Flags&ast.SymbolFlagsModule == 0 {
					continue
				}
			} else if autoImport.Export.Flags&ast.SymbolFlagsValue == 0 {
				continue
			}
		}

		entry := l.createLSPCompletionItem(
			ctx,
			autoImport.Fix.Name,
			"",
			"",
			SortTextAutoImportSuggestions,
			autoImport.Export.ScriptElementKind,
			autoImport.Export.ScriptElementKindModifiers,
			nil,
			nil,
			&lsproto.CompletionItemLabelDetails{
				Description: new(autoImport.Fix.ModuleSpecifier),
			},
			file,
			position,
			false, /*isMemberCompletion*/
			false, /*isSnippet*/
			true,  /*hasAction*/
			false, /*preselect*/
			autoImport.Fix.ModuleSpecifier,
			autoImport.Fix.AutoImportFix,
			nil, /*detail*/
		)

		if isShadowed, _ := uniques[autoImport.Fix.Name]; !isShadowed {
			uniques[autoImport.Fix.Name] = false
			sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: entry})
		}
	}

	uniqueSet := collections.NewSetWithSizeHint[string](len(uniques))
	for name := range uniques {
		uniqueSet.Add(name)
	}
	return *uniqueSet, sortedEntries
}

func completionNameForLiteral(
	file *ast.SourceFile,
	preferences lsutil.UserPreferences,
	literal literalValue,
) string {
	switch literal := literal.(type) {
	case string:
		return quote(file, preferences, literal)
	case jsnum.Number:
		name, _ := core.StringifyJson(literal, "" /*prefix*/, "" /*suffix*/)
		return name
	case jsnum.PseudoBigInt:
		return literal.String() + "n"
	}
	panic(fmt.Sprintf("Unhandled literal value: %v", literal))
}

func createCompletionItemForLiteral(
	file *ast.SourceFile,
	preferences lsutil.UserPreferences,
	literal literalValue,
) *lsproto.CompletionItem {
	return &lsproto.CompletionItem{
		Label:            completionNameForLiteral(file, preferences, literal),
		Kind:             new(lsproto.CompletionItemKindConstant),
		SortText:         new(string(SortTextLocationPriority)),
		CommitCharacters: new([]string{}),
	}
}

func (l *LanguageService) createCompletionItem(
	ctx context.Context,
	typeChecker *checker.Checker,
	symbol *ast.Symbol,
	sortText SortText,
	replacementToken *ast.Node,
	data *completionDataData,
	position int,
	file *ast.SourceFile,
	name string,
	needsConvertPropertyAccess bool,
	origin *symbolOriginInfo,
	useSemicolons bool,
	compilerOptions *core.CompilerOptions,
	isMemberCompletion bool,
) *lsproto.CompletionItem {
	contextToken := data.contextToken
	var insertText string
	var filterText string
	replacementSpan := l.getReplacementRangeForContextToken(file, replacementToken, position)
	var isSnippet, hasAction bool
	source := getSourceFromOrigin(origin)
	var labelDetails *lsproto.CompletionItemLabelDetails
	preferences := l.UserPreferences()
	insertQuestionDot := originIsNullableMember(origin)
	useBraces := originIsSymbolMember(origin) || needsConvertPropertyAccess
	if originIsThisTypeNode(origin) {
		if needsConvertPropertyAccess {
			insertText = fmt.Sprintf(
				"this%s[%s]",
				core.IfElse(insertQuestionDot, "?.", ""),
				quotePropertyName(file, preferences, name),
			)
		} else {
			insertText = fmt.Sprintf(
				"this%s%s",
				core.IfElse(insertQuestionDot, "?.", "."),
				name,
			)
		}
	} else if data.propertyAccessToConvert != nil && (useBraces || insertQuestionDot) {
		// We should only have needsConvertPropertyAccess if there's a property access to convert. But see microsoft/TypeScript#21790.
		// Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro.
		if useBraces {
			if needsConvertPropertyAccess {
				insertText = fmt.Sprintf("[%s]", quotePropertyName(file, preferences, name))
			} else {
				insertText = fmt.Sprintf("[%s]", name)
			}
		} else {
			insertText = name
		}

		if insertQuestionDot || data.propertyAccessToConvert.QuestionDotToken() != nil {
			insertText = "?." + insertText
		}

		dot := astnav.FindChildOfKind(data.propertyAccessToConvert, ast.KindDotToken, file)
		if dot == nil {
			dot = astnav.FindChildOfKind(data.propertyAccessToConvert, ast.KindQuestionDotToken, file)
		}

		if dot == nil {
			return nil
		}

		// If the text after the '.' starts with this name, write over it. Else, add new text.
		var end int
		if strings.HasPrefix(name, data.propertyAccessToConvert.Name().Text()) {
			end = data.propertyAccessToConvert.End()
		} else {
			end = dot.End()
		}
		replacementSpan = new(l.createLspRangeFromBounds(astnav.GetStartOfNode(dot, file, false /*includeJSDoc*/), end, file))
	}

	if data.jsxInitializer.isInitializer {
		if insertText == "" {
			insertText = name
		}
		insertText = fmt.Sprintf("{%s}", insertText)
		if data.jsxInitializer.initializer != nil {
			replacementSpan = new(l.createLspRangeFromNode(data.jsxInitializer.initializer, file))
		}
	}

	if originIsPromise(origin) && data.propertyAccessToConvert != nil {
		if insertText == "" {
			insertText = name
		}
		precedingToken := astnav.FindPrecedingToken(file, data.propertyAccessToConvert.Pos())
		var awaitText string
		if precedingToken != nil && lsutil.PositionIsASICandidate(precedingToken.End(), precedingToken.Parent, file) {
			awaitText = ";"
		}

		awaitText += "(await " + scanner.GetTextOfNode(data.propertyAccessToConvert.Expression()) + ")"
		if needsConvertPropertyAccess {
			insertText = awaitText + insertText
		} else {
			dotStr := core.IfElse(insertQuestionDot, "?.", ".")
			insertText = awaitText + dotStr + insertText
		}
		isInAwaitExpression := ast.IsAwaitExpression(data.propertyAccessToConvert.Parent)
		wrapNode := core.IfElse(
			isInAwaitExpression,
			data.propertyAccessToConvert.Parent,
			data.propertyAccessToConvert.Expression(),
		)
		replacementSpan = new(l.createLspRangeFromBounds(
			astnav.GetStartOfNode(wrapNode, file, false /*includeJSDoc*/),
			data.propertyAccessToConvert.End(),
			file,
		))
	}

	if originIsTypeOnlyAlias(origin) {
		hasAction = true
	}

	// Provide object member completions when missing commas, and insert missing commas.
	// For example:
	//
	//    interface I {
	//        a: string;
	//        b: number
	//     }
	//
	//     const cc: I = { a: "red" | }
	//
	// Completion should add a comma after "red" and provide completions for b
	if data.completionKind == CompletionKindObjectPropertyDeclaration &&
		contextToken != nil &&
		!ast.NodeHasKind(astnav.FindPrecedingTokenEx(file, contextToken.Pos(), contextToken, false /*excludeJSDoc*/), ast.KindCommaToken) {
		if ast.IsMethodDeclaration(contextToken.Parent.Parent) ||
			ast.IsGetAccessorDeclaration(contextToken.Parent.Parent) ||
			ast.IsSetAccessorDeclaration(contextToken.Parent.Parent) ||
			ast.IsSpreadAssignment(contextToken.Parent) ||
			lsutil.GetLastToken(ast.FindAncestor(contextToken.Parent, ast.IsPropertyAssignment), file) == contextToken ||
			ast.IsShorthandPropertyAssignment(contextToken.Parent) &&
				getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position) {
			source = string(completionSourceObjectLiteralMemberWithComma)
			hasAction = true
		}
	}

	if preferences.IncludeCompletionsWithClassMemberSnippets.IsTrue() &&
		data.completionKind == CompletionKindMemberLike &&
		isClassLikeMemberCompletion(symbol, data.location, file) {
		// !!! class member completions
	}

	if originIsObjectLiteralMethod(origin) {
		insertText = origin.asObjectLiteralMethod().insertText
		isSnippet = origin.asObjectLiteralMethod().isSnippet
		labelDetails = origin.asObjectLiteralMethod().labelDetails // !!! check if this can conflict with case above where we set label details
		if !clientSupportsItemLabelDetails(ctx) {
			name = name + *origin.asObjectLiteralMethod().labelDetails.Detail
			labelDetails = nil
		}
		source = string(completionSourceObjectLiteralMethodSnippet)
		sortText = sortBelow(sortText)
	}

	if data.isJsxIdentifierExpected &&
		!data.isRightOfOpenTag &&
		clientSupportsItemSnippet(ctx) &&
		preferences.JsxAttributeCompletionStyle != lsutil.JsxAttributeCompletionStyleNone &&
		!(data.location.Parent != nil && ast.IsJsxAttribute(data.location.Parent) && data.location.Parent.Initializer() != nil) {
		useBraces := preferences.JsxAttributeCompletionStyle == lsutil.JsxAttributeCompletionStyleBraces
		t := typeChecker.GetTypeOfSymbolAtLocation(symbol, data.location)

		// If is boolean like or undefined, don't return a snippet, we want to return just the completion.
		if preferences.JsxAttributeCompletionStyle == lsutil.JsxAttributeCompletionStyleAuto &&
			!t.IsBooleanLike() &&
			!(t.IsUnion() && core.Some(t.Types(), (*checker.Type).IsBooleanLike)) {
			if t.IsStringLike() ||
				t.IsUnion() &&
					core.Every(
						t.Types(),
						func(t *checker.Type) bool {
							return t.Flags()&(checker.TypeFlagsStringLike|checker.TypeFlagsUndefined) != 0 ||
								isStringAndEmptyAnonymousObjectIntersection(typeChecker, t)
						},
					) {
				// If type is string-like or undefined, use quotes.
				insertText = fmt.Sprintf("%s=%s", escapeSnippetText(name), quote(file, preferences, "$1"))
				isSnippet = true
			} else {
				// Use braces for everything else.
				useBraces = true
			}
		}

		if useBraces {
			insertText = escapeSnippetText(name) + "={$1}"
			isSnippet = true
		}
	}

	parentNamedImportOrExport := ast.FindAncestor(data.location, isNamedImportsOrExports)
	if parentNamedImportOrExport != nil {
		if !scanner.IsIdentifierText(name, core.LanguageVariantStandard) {
			insertText = quotePropertyName(file, preferences, name)

			if parentNamedImportOrExport.Kind == ast.KindNamedImports {
				// Check if it is `import { ^here as name } from '...'``.
				// We have to access the scanner here to check if it is `{ ^here as name }`` or `{ ^here, as, name }`.
				scanner := scanner.NewScanner()
				scanner.SetText(file.Text())
				scanner.ResetPos(position)
				if !(scanner.Scan() == ast.KindAsKeyword && scanner.Scan() == ast.KindIdentifier) {
					insertText += " as " + generateIdentifierForArbitraryString(name)
				}
			}
		} else if parentNamedImportOrExport.Kind == ast.KindNamedImports {
			possibleToken := scanner.StringToToken(name)
			if possibleToken != ast.KindUnknown &&
				(possibleToken == ast.KindAwaitKeyword || lsutil.IsNonContextualKeyword(possibleToken)) {
				insertText = fmt.Sprintf("%s as %s_", name, name)
			}
		}
	}

	// Commit characters

	elementKind := lsutil.GetSymbolKind(typeChecker, symbol, data.location)
	var commitCharacters *[]string
	if clientSupportsItemCommitCharacters(ctx) {
		if elementKind == lsutil.ScriptElementKindWarning || elementKind == lsutil.ScriptElementKindString {
			commitCharacters = &[]string{}
		} else if !clientSupportsDefaultCommitCharacters(ctx) {
			commitCharacters = new(data.defaultCommitCharacters)
		}
		// Otherwise use the completion list default.
	}

	preselect := isRecommendedCompletionMatch(symbol, data.recommendedCompletion, typeChecker)
	kindModifiers := lsutil.GetSymbolModifiers(typeChecker, symbol)

	return l.createLSPCompletionItem(
		ctx,
		name,
		insertText,
		filterText,
		sortText,
		elementKind,
		kindModifiers,
		replacementSpan,
		commitCharacters,
		labelDetails,
		file,
		position,
		isMemberCompletion,
		isSnippet,
		hasAction,
		preselect,
		source,
		nil, /*autoImportFix*/
		nil, /*detail*/
	)
}

func isRecommendedCompletionMatch(localSymbol *ast.Symbol, recommendedCompletion *ast.Symbol, typeChecker *checker.Checker) bool {
	return localSymbol == recommendedCompletion ||
		localSymbol.Flags&ast.SymbolFlagsExportValue != 0 && typeChecker.GetExportSymbolOfSymbol(localSymbol) == recommendedCompletion
}

// Ported from vscode.
var wordSeparators = collections.NewSetFromItems(
	'`', '~', '!', '@', '%', '^', '&', '*', '(', ')', '-', '=', '+', '[', '{', ']', '}', '\\', '|',
	';', ':', '\'', '"', ',', '.', '<', '>', '/', '?',
)

// Finds the length and first rune of the word that ends at the given position.
// e.g. for "abc def.ghi|jkl", the word length is 3 and the word start is 'g'.
func getWordLengthAndStart(sourceFile *ast.SourceFile, position int) (wordLength int, wordStart rune) {
	// !!! Port other case of vscode's `DEFAULT_WORD_REGEXP` that covers words that start like numbers, e.g. -123.456abcd.
	text := sourceFile.Text()[:position]
	totalSize := 0
	var firstRune rune
	for r, size := utf8.DecodeLastRuneInString(text); size != 0; r, size = utf8.DecodeLastRuneInString(text[:len(text)-totalSize]) {
		if wordSeparators.Has(r) || unicode.IsSpace(r) {
			break
		}
		totalSize += size
		firstRune = r
	}
	// If word starts with `@`, disregard this first character.
	if firstRune == '@' {
		totalSize -= 1
		firstRune, _ = utf8.DecodeRuneInString(text[len(text)-totalSize:])
	}
	return totalSize, firstRune
}

// `["ab c"]` -> `ab c`
// `['ab c']` -> `ab c`
// `[123]` -> `123`
func trimElementAccess(text string) string {
	text = strings.TrimPrefix(text, "[")
	text = strings.TrimSuffix(text, "]")
	if strings.HasPrefix(text, `'`) && strings.HasSuffix(text, `'`) {
		text = strings.TrimPrefix(strings.TrimSuffix(text, `'`), `'`)
	}
	if strings.HasPrefix(text, `"`) && strings.HasSuffix(text, `"`) {
		text = strings.TrimPrefix(strings.TrimSuffix(text, `"`), `"`)
	}
	return text
}

// Ported from vscode ts extension: `getFilterText`.
func getFilterText(
	file *ast.SourceFile,
	position int,
	insertText string,
	label string,
	wordStart rune,
	dotAccessor string,
) string {
	// Private field completion, e.g. label `#bar`.
	if after, ok := strings.CutPrefix(label, "#"); ok {
		if insertText != "" {
			if after, ok := strings.CutPrefix(insertText, "this.#"); ok {
				if wordStart == '#' {
					// `method() { this.#| }`
					// `method() { #| }`
					return ""
				} else {
					// `method() { this.| }`
					// `method() { | }`
					return after
				}
			}
		} else {
			if wordStart == '#' {
				// `method() { this.#| }`
				return ""
			} else {
				// `method() { this.| }`
				// `method() { | }`
				return after
			}
		}
	}

	// For `this.` completions, generally don't set the filter text since we don't want them to be overly deprioritized. microsoft/vscode#74164
	if strings.HasPrefix(insertText, "this.") {
		return ""
	}

	// Handle the case:
	// ```
	// const xyz = { 'ab c': 1 };
	// xyz.ab|
	// ```
	// In which case we want to insert a bracket accessor but should use `.abc` as the filter text instead of
	// the bracketed insert text.
	if strings.HasPrefix(insertText, "[") {
		return dotAccessor + trimElementAccess(insertText)
	}

	if strings.HasPrefix(insertText, "?.") {
		// Handle this case like the case above:
		// ```
		// const xyz = { 'ab c': 1 } | undefined;
		// xyz.ab|
		// ```
		// filterText should be `.ab c` instead of `?.['ab c']`.
		if strings.HasPrefix(insertText, "?.[") {
			return dotAccessor + trimElementAccess(insertText[2:])
		} else {
			// ```
			// const xyz = { abc: 1 } | undefined;
			// xyz.ab|
			// ```
			// filterText should be `.abc` instead of `?.abc.
			return dotAccessor + insertText[2:]
		}
	}

	// In all other cases, fall back to using the insertText.
	return insertText
}

// Ported from vscode's `provideCompletionItems`.
func getDotAccessor(file *ast.SourceFile, position int) string {
	text := file.Text()[:position]
	totalSize := 0
	if strings.HasSuffix(text, "?.") {
		totalSize += 2
		return file.Text()[position-totalSize : position]
	}
	if strings.HasSuffix(text, ".") {
		totalSize += 1
		return file.Text()[position-totalSize : position]
	}
	return ""
}

func strPtrIsEmpty(ptr *string) bool {
	if ptr == nil {
		return true
	}
	return *ptr == ""
}

func strPtrTo(v string) *string {
	if v == "" {
		return nil
	}
	return &v
}

func boolToPtr(v bool) *bool {
	if v {
		return new(true)
	}
	return nil
}

func getLineOfPosition(file *ast.SourceFile, pos int) int {
	line := scanner.GetECMALineOfPosition(file, pos)
	return line
}

func getLineEndOfPosition(file *ast.SourceFile, pos int) int {
	line := getLineOfPosition(file, pos)
	lineStarts := scanner.GetECMALineStarts(file)
	var lastCharPos int
	if line+1 >= len(lineStarts) {
		lastCharPos = file.End()
	} else {
		lastCharPos = int(lineStarts[line+1]) - 1
	}
	fullText := file.Text()
	if lastCharPos > 0 && lastCharPos < len(fullText) && fullText[lastCharPos] == '\n' && fullText[lastCharPos-1] == '\r' {
		return lastCharPos - 1
	}
	return lastCharPos
}

func isClassLikeMemberCompletion(symbol *ast.Symbol, location *ast.Node, file *ast.SourceFile) bool {
	// !!! class member completions
	return false
}

func symbolAppearsToBeTypeOnly(symbol *ast.Symbol, typeChecker *checker.Checker) bool {
	flags := checker.SkipAlias(symbol, typeChecker).CombinedLocalAndExportSymbolFlags()
	return flags&ast.SymbolFlagsValue == 0 &&
		(len(symbol.Declarations) == 0 || !ast.IsInJSFile(symbol.Declarations[0]) || flags&ast.SymbolFlagsType != 0)
}

func shouldIncludeSymbol(
	symbol *ast.Symbol,
	data *completionDataData,
	closestSymbolDeclaration *ast.Declaration,
	file *ast.SourceFile,
	typeChecker *checker.Checker,
	compilerOptions *core.CompilerOptions,
) bool {
	allFlags := symbol.Flags
	location := data.location
	// export = /**/ here we want to get all meanings, so any symbol is ok
	if location.Parent != nil && ast.IsExportAssignment(location.Parent) {
		return true
	}

	// Filter out variables from their own initializers
	// `const a = /* no 'a' here */`
	if closestSymbolDeclaration != nil &&
		ast.IsVariableDeclaration(closestSymbolDeclaration) &&
		symbol.ValueDeclaration == closestSymbolDeclaration {
		return false
	}

	// Filter out current and latter parameters from defaults
	// `function f(a = /* no 'a' and 'b' here */, b) { }` or
	// `function f<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.IsParameterDeclaration(closestSymbolDeclaration) && ast.IsParameterDeclaration(symbolDeclaration) {
			parameters := closestSymbolDeclaration.Parent.ParameterList()
			if symbolDeclaration.Pos() >= closestSymbolDeclaration.Pos() &&
				symbolDeclaration.Pos() < parameters.End() {
				return false
			}
		} else if ast.IsTypeParameterDeclaration(closestSymbolDeclaration) &&
			ast.IsTypeParameterDeclaration(symbolDeclaration) {
			if closestSymbolDeclaration == symbolDeclaration && data.contextToken != nil && data.contextToken.Kind == ast.KindExtendsKeyword {
				// filter out the directly self-recursive type parameters
				// `type A<K 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 &&
		symbol != symbolOrigin &&
		data.symbolToSortTextMap[ast.GetSymbolId(symbol)] == SortTextGlobalsOrKeywords &&
		symbol.Parent != nil && checker.IsExternalModuleSymbol(symbol.Parent) {
		return false
	}

	allFlags = allFlags | symbolOrigin.CombinedLocalAndExportSymbolFlags()
	if symbol.Flags&ast.SymbolFlagsAlias != 0 {
		allFlags = allFlags | typeChecker.GetSymbolFlags(symbol)
	}

	// import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace)
	if isInRightSideOfInternalImportEqualsDeclaration(data.location) {
		return allFlags&ast.SymbolFlagsNamespace != 0
	}

	if data.isTypeOnlyLocation {
		// It's a type, but you can reach it by namespace.type as well.
		return symbolCanBeReferencedAtTypeLocation(symbol, typeChecker, collections.Set[ast.SymbolId]{})
	}

	// expressions are value space (which includes the value namespaces)
	return allFlags&ast.SymbolFlagsValue != 0
}

func getCompletionEntryDisplayNameForSymbol(
	symbol *ast.Symbol,
	origin *symbolOriginInfo,
	completionKind CompletionKind,
	isJsxIdentifierExpected bool,
) (displayName string, needsConvertPropertyAccess bool) {
	if originIsIgnore(origin) {
		return "", false
	}

	var name string
	if originIncludesSymbolName(origin) {
		name = origin.symbolName()
	} else {
		name = ast.SymbolName(symbol)
	}
	if name == "" ||
		// If the symbol is external module, don't show it in the completion list
		// (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there)
		symbol.Flags&ast.SymbolFlagsModule != 0 && startsWithQuote(name) ||
		// If the symbol is the internal name of an ES symbol, it is not a valid entry. Internal names for ES symbols start with "__@"
		checker.IsKnownSymbol(symbol) {
		return "", false
	}

	variant := core.IfElse(isJsxIdentifierExpected, core.LanguageVariantJSX, core.LanguageVariantStandard)
	// name is a valid identifier or private identifier text
	if scanner.IsIdentifierText(name, variant) ||
		symbol.ValueDeclaration != nil && ast.IsPrivateIdentifierClassElementDeclaration(symbol.ValueDeclaration) {
		return name, false
	}
	if symbol.Flags&ast.SymbolFlagsAlias != 0 {
		// Allow non-identifier import/export aliases since we can insert them as string literals
		return name, true
	}

	switch completionKind {
	case CompletionKindMemberLike:
		if originIsComputedPropertyName(origin) {
			return origin.symbolName(), false
		}
		return "", false
	case CompletionKindObjectPropertyDeclaration:
		// TODO: microsoft/TypeScript#18169
		escapedName, _ := core.StringifyJson(name, "", "")
		return escapedName, false
	case CompletionKindPropertyAccess, CompletionKindGlobal:
		// For a 'this.' completion it will be in a global context, but may have a non-identifier name.
		// Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547
		ch, _ := utf8.DecodeRuneInString(name)
		if ch == ' ' {
			return "", false
		}
		return name, true
	case CompletionKindNone, CompletionKindString:
		return name, false
	default:
		panic(fmt.Sprintf("Unexpected completion kind: %v", completionKind))
	}
}

// !!! refactor symbolOriginInfo so that we can tell the difference between flags and the kind of data it has
func originIsIgnore(origin *symbolOriginInfo) bool {
	return origin != nil && origin.kind&symbolOriginInfoKindIgnore != 0
}

func originIncludesSymbolName(origin *symbolOriginInfo) bool {
	return originIsComputedPropertyName(origin)
}

func originIsComputedPropertyName(origin *symbolOriginInfo) bool {
	return origin != nil && origin.kind&symbolOriginInfoKindComputedPropertyName != 0
}

func originIsObjectLiteralMethod(origin *symbolOriginInfo) bool {
	return origin != nil && origin.kind&symbolOriginInfoKindObjectLiteralMethod != 0
}

func originIsThisTypeNode(origin *symbolOriginInfo) bool {
	return origin != nil && origin.kind&symbolOriginInfoKindThisType != 0
}

func originIsTypeOnlyAlias(origin *symbolOriginInfo) bool {
	return origin != nil && origin.kind&symbolOriginInfoKindTypeOnlyAlias != 0
}

func originIsSymbolMember(origin *symbolOriginInfo) bool {
	return origin != nil && origin.kind&symbolOriginInfoKindSymbolMember != 0
}

func originIsNullableMember(origin *symbolOriginInfo) bool {
	return origin != nil && origin.kind&symbolOriginInfoKindNullable != 0
}

func originIsPromise(origin *symbolOriginInfo) bool {
	return origin != nil && origin.kind&symbolOriginInfoKindPromise != 0
}

func getSourceFromOrigin(origin *symbolOriginInfo) string {
	if originIsThisTypeNode(origin) {
		return string(completionSourceThisProperty)
	}

	if originIsTypeOnlyAlias(origin) {
		return string(completionSourceTypeOnlyAlias)
	}

	return ""
}

// In a scenarion such as `const x = 1 * |`, the context and previous tokens are both `*`.
// In `const x = 1 * o|`, the context token is *, and the previous token is `o`.
// `contextToken` and `previousToken` can both be nil if we are at the beginning of the file.
func getRelevantTokens(position int, file *ast.SourceFile) (contextToken *ast.Node, previousToken *ast.Node) {
	previousToken = astnav.FindPrecedingToken(file, position)
	if previousToken != nil && position <= previousToken.End() && (ast.IsMemberName(previousToken) || ast.IsKeywordKind(previousToken.Kind)) {
		contextToken := astnav.FindPrecedingToken(file, previousToken.Pos())
		return contextToken, previousToken
	}
	return previousToken, previousToken
}

// "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " " | "*"
type CompletionsTriggerCharacter = string

func isValidTrigger(file *ast.SourceFile, triggerCharacter CompletionsTriggerCharacter, contextToken *ast.Node, position int) bool {
	switch triggerCharacter {
	case ".", "@":
		return true
	case "\"", "'", "`":
		// Only automatically bring up completions if this is an opening quote.
		return contextToken != nil &&
			isStringLiteralOrTemplate(contextToken) &&
			position == astnav.GetStartOfNode(contextToken, file, false /*includeJSDoc*/)+1
	case "#":
		return contextToken != nil &&
			ast.IsPrivateIdentifier(contextToken) &&
			ast.GetContainingClass(contextToken) != nil
	case "<":
		// Opening JSX tag
		return contextToken != nil &&
			contextToken.Kind == ast.KindLessThanToken &&
			(!ast.IsBinaryExpression(contextToken.Parent) || binaryExpressionMayBeOpenTag(contextToken.Parent.AsBinaryExpression()))
	case "/":
		if contextToken == nil {
			return false
		}
		if ast.IsStringLiteralLike(contextToken) {
			return ast.TryGetImportFromModuleSpecifier(contextToken) != nil
		}
		return contextToken.Kind == ast.KindLessThanSlashToken && ast.IsJsxClosingElement(contextToken.Parent)
	case " ":
		return contextToken != nil && contextToken.Kind == ast.KindImportKeyword && contextToken.Parent.Kind == ast.KindSourceFile
	case "*":
		return isPotentiallyValidJSDocSnippetCompletionPosition(file, position)
	default:
		panic("Unknown trigger character: " + triggerCharacter)
	}
}

func isStringLiteralOrTemplate(node *ast.Node) bool {
	switch node.Kind {
	case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral, ast.KindTemplateExpression,
		ast.KindTaggedTemplateExpression:
		return true
	}
	return false
}

func binaryExpressionMayBeOpenTag(binaryExpression *ast.BinaryExpression) bool {
	return ast.NodeIsMissing(binaryExpression.Left)
}

func isCheckedFile(file *ast.SourceFile, compilerOptions *core.CompilerOptions) bool {
	return !ast.IsSourceFileJS(file) || ast.IsCheckJSEnabledForFile(file, compilerOptions)
}

func isContextTokenValueLocation(contextToken *ast.Node) bool {
	return contextToken != nil && ((contextToken.Kind == ast.KindTypeOfKeyword &&
		(contextToken.Parent.Kind == ast.KindTypeQuery || ast.IsTypeOfExpression(contextToken.Parent))) ||
		(contextToken.Kind == ast.KindAssertsKeyword && contextToken.Parent.Kind == ast.KindTypePredicate))
}

func isPossiblyTypeArgumentPosition(token *ast.Node, sourceFile *ast.SourceFile, typeChecker *checker.Checker) bool {
	info := getPossibleTypeArgumentsInfo(token, sourceFile)
	return info != nil && (ast.IsPartOfTypeNode(info.called) ||
		len(getPossibleGenericSignatures(info.called, info.nTypeArguments, typeChecker)) != 0 ||
		isPossiblyTypeArgumentPosition(info.called, sourceFile, typeChecker))
}

func isContextTokenTypeLocation(contextToken *ast.Node) bool {
	if contextToken != nil {
		parentKind := contextToken.Parent.Kind
		switch contextToken.Kind {
		case ast.KindColonToken:
			return parentKind == ast.KindPropertyDeclaration ||
				parentKind == ast.KindPropertySignature ||
				parentKind == ast.KindParameter ||
				parentKind == ast.KindVariableDeclaration ||
				ast.IsFunctionLikeKind(parentKind)
		case ast.KindEqualsToken:
			return parentKind == ast.KindTypeAliasDeclaration || parentKind == ast.KindTypeParameter
		case ast.KindAsKeyword:
			return parentKind == ast.KindAsExpression
		case ast.KindLessThanToken:
			return parentKind == ast.KindTypeReference || parentKind == ast.KindTypeAssertionExpression
		case ast.KindExtendsKeyword:
			return parentKind == ast.KindTypeParameter
		case ast.KindSatisfiesKeyword:
			return parentKind == ast.KindSatisfiesExpression
		}
	}
	return false
}

// True if symbol is a type or a module containing at least one type.
func symbolCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules collections.Set[ast.SymbolId]) bool {
	// Since an alias can be merged with a local declaration, we need to test both the alias and its target.
	// This code used to just test the result of `skipAlias`, but that would ignore any locally introduced meanings.
	return nonAliasCanBeReferencedAtTypeLocation(symbol, typeChecker, seenModules) ||
		nonAliasCanBeReferencedAtTypeLocation(
			checker.SkipAlias(core.IfElse(symbol.ExportSymbol != nil, symbol.ExportSymbol, symbol), typeChecker),
			typeChecker,
			seenModules,
		)
}

func nonAliasCanBeReferencedAtTypeLocation(symbol *ast.Symbol, typeChecker *checker.Checker, seenModules collections.Set[ast.SymbolId]) bool {
	return symbol.Flags&ast.SymbolFlagsType != 0 || typeChecker.IsUnknownSymbol(symbol) ||
		symbol.Flags&ast.SymbolFlagsModule != 0 && seenModules.AddIfAbsent(ast.GetSymbolId(symbol)) &&
			core.Some(
				typeChecker.GetExportsOfModule(symbol),
				func(e *ast.Symbol) bool { return symbolCanBeReferencedAtTypeLocation(e, typeChecker, seenModules) },
			)
}

// Gets all properties on a type, but if that type is a union of several types,
// excludes array-like types or callable/constructable types.
func getPropertiesForCompletion(t *checker.Type, typeChecker *checker.Checker) []*ast.Symbol {
	if t.IsUnion() {
		return core.CheckEachDefined(typeChecker.GetAllPossiblePropertiesOfTypes(t.Types()), "getAllPossiblePropertiesOfTypes() should all be defined.")
	} else {
		return core.CheckEachDefined(typeChecker.GetApparentProperties(t), "getApparentProperties() should all be defined.")
	}
}

// Given 'a.b.c', returns 'a'.
func getLeftMostName(e *ast.Expression) *ast.IdentifierNode {
	if ast.IsIdentifier(e) {
		return e
	} else if ast.IsPropertyAccessExpression(e) {
		return getLeftMostName(e.Expression())
	} else {
		return nil
	}
}

func getFirstSymbolInChain(symbol *ast.Symbol, enclosingDeclaration *ast.Node, typeChecker *checker.Checker) *ast.Symbol {
	chain := typeChecker.GetAccessibleSymbolChain(
		symbol,
		enclosingDeclaration,
		ast.SymbolFlagsAll, /*meaning*/
		false,              /*useOnlyExternalAliasing*/
	)
	if len(chain) > 0 {
		return chain[0]
	}
	if symbol.Parent != nil {
		if isModuleSymbol(symbol.Parent) {
			return symbol
		}
		return getFirstSymbolInChain(symbol.Parent, enclosingDeclaration, typeChecker)
	}
	return nil
}

func isModuleSymbol(symbol *ast.Symbol) bool {
	return core.Some(symbol.Declarations, func(decl *ast.Declaration) bool { return decl.Kind == ast.KindSourceFile })
}

func getNullableSymbolOriginInfoKind(kind symbolOriginInfoKind, insertQuestionDot bool) symbolOriginInfoKind {
	if insertQuestionDot {
		kind |= symbolOriginInfoKindNullable
	}
	return kind
}

func isStaticProperty(symbol *ast.Symbol) bool {
	return symbol.ValueDeclaration != nil &&
		symbol.ValueDeclaration.ModifierFlags()&ast.ModifierFlagsStatic != 0 &&
		ast.IsClassLike(symbol.ValueDeclaration.Parent)
}

// getContextualTypeForConditionalExpression handles completion within a conditional expression
// (ternary operator) by using the parent expression to find the contextual type.
func getContextualTypeForConditionalExpression(conditionalExpr *ast.Node, position int, file *ast.SourceFile, typeChecker *checker.Checker) *checker.Type {
	argInfo := getArgumentInfoForCompletions(conditionalExpr, position, file, typeChecker)
	if argInfo != nil {
		return typeChecker.GetContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex)
	}
	// Fall through to regular contextual type logic if not in an argument
	contextualType := typeChecker.GetContextualType(conditionalExpr, checker.ContextFlagsIgnoreNodeInferences)
	if contextualType != nil {
		return contextualType
	}
	return typeChecker.GetContextualType(conditionalExpr, checker.ContextFlagsNone)
}

func getContextualType(previousToken *ast.Node, position int, file *ast.SourceFile, typeChecker *checker.Checker) *checker.Type {
	parent := previousToken.Parent
	switch previousToken.Kind {
	case ast.KindIdentifier:
		return getContextualTypeFromParent(previousToken, typeChecker, checker.ContextFlagsNone)
	case ast.KindEqualsToken:
		switch parent.Kind {
		case ast.KindVariableDeclaration:
			return typeChecker.GetContextualType(parent.Initializer(), checker.ContextFlagsNone)
		case ast.KindBinaryExpression:
			return typeChecker.GetTypeAtLocation(parent.AsBinaryExpression().Left)
		case ast.KindJsxAttribute:
			return typeChecker.GetContextualTypeForJsxAttribute(parent)
		default:
			return nil
		}
	case ast.KindNewKeyword:
		return typeChecker.GetContextualType(parent, checker.ContextFlagsNone)
	case ast.KindCaseKeyword:
		caseClause := core.IfElse(ast.IsCaseClause(parent), parent, nil)
		if caseClause != nil {
			return getSwitchedType(caseClause, typeChecker)
		}
		return nil
	case ast.KindOpenBraceToken:
		if ast.IsJsxExpression(parent) && !ast.IsJsxElement(parent.Parent) && !ast.IsJsxFragment(parent.Parent) {
			return typeChecker.GetContextualTypeForJsxAttribute(parent.Parent)
		}
		return nil
	case ast.KindOpenBracketToken:
		// When completing after `[` in an array literal (e.g., `[/*here*/]`),
		// we should provide contextual type for the first element
		if ast.IsArrayLiteralExpression(parent) {
			contextualArrayType := typeChecker.GetContextualType(parent, checker.ContextFlagsNone)
			if contextualArrayType != nil {
				// Get the type for the first element (index 0)
				return typeChecker.GetContextualTypeForArrayLiteralAtPosition(contextualArrayType, parent, position)
			}
		}
		return nil
	case ast.KindCloseBracketToken:
		// When completing after `]` (e.g., `[x]/*here*/`), we should not provide a contextual type
		// for the closing bracket token itself. Without this case, CloseBracketToken would fall through
		// to the default case, and if the parent is an array literal, GetContextualType would try to
		// find the token's index in the array elements (returning -1), leading to an out-of-bounds panic
		// in getContextualTypeForElementExpression.
		return nil
	case ast.KindQuestionToken:
		// When completing after `?` in a ternary conditional (e.g., `foo(a ? /*here*/)`),
		// we need to look at the parent conditional expression to find the contextual type.
		if ast.IsConditionalExpression(parent) {
			return getContextualTypeForConditionalExpression(parent, position, file, typeChecker)
		}
		return nil
	case ast.KindColonToken:
		// When completing after `:` in a ternary conditional (e.g., `foo(a ? b : /*here*/)`),
		// we need to look at the parent conditional expression to find the contextual type.
		// Only handle this if parent is ConditionalExpression, otherwise fall through to default
		// (colons are used in other contexts like object literals, type annotations, etc.)
		if ast.IsConditionalExpression(parent) {
			return getContextualTypeForConditionalExpression(parent, position, file, typeChecker)
		}
	case ast.KindCommaToken:
		// When completing after `,` in an array literal (e.g., `[x, /*here*/]`),
		// we should provide contextual type for the element after the comma.
		if ast.IsArrayLiteralExpression(parent) {
			contextualArrayType := typeChecker.GetContextualType(parent, checker.ContextFlagsNone)
			if contextualArrayType != nil {
				return typeChecker.GetContextualTypeForArrayLiteralAtPosition(contextualArrayType, parent, position)
			}
			return nil
		}
	}
	// Default case: see if we're in an argument position.
	argInfo := getArgumentInfoForCompletions(previousToken, position, file, typeChecker)
	if argInfo != nil {
		return typeChecker.GetContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex)
	} else if isEqualityOperatorKind(previousToken.Kind) && ast.IsBinaryExpression(parent) && isEqualityOperatorKind(parent.AsBinaryExpression().OperatorToken.Kind) {
		// completion at `x ===/**/`
		return typeChecker.GetTypeAtLocation(parent.AsBinaryExpression().Left)
	} else {
		contextualType := typeChecker.GetContextualType(previousToken, checker.ContextFlagsIgnoreNodeInferences)
		if contextualType != nil {
			return contextualType
		}
		return typeChecker.GetContextualType(previousToken, checker.ContextFlagsNone)
	}
}

func getSwitchedType(caseClause *ast.CaseOrDefaultClauseNode, typeChecker *checker.Checker) *checker.Type {
	return typeChecker.GetTypeAtLocation(caseClause.Parent.Parent.Expression())
}

func isEqualityOperatorKind(kind ast.Kind) bool {
	switch kind {
	case ast.KindEqualsEqualsEqualsToken, ast.KindEqualsEqualsToken,
		ast.KindExclamationEqualsEqualsToken, ast.KindExclamationEqualsToken:
		return true
	default:
		return false
	}
}

// We disregard boolean literals for completion purposes.
func isLiteral(t *checker.Type) bool {
	return t.IsStringLiteral() || t.IsNumberLiteral() || t.IsBigIntLiteral()
}

func getRecommendedCompletion(previousToken *ast.Node, contextualType *checker.Type, typeChecker *checker.Checker) *ast.Symbol {
	var types []*checker.Type
	if contextualType.IsUnion() {
		types = contextualType.Types()
	} else {
		types = []*checker.Type{contextualType}
	}
	// For a union, return the first one with a recommended completion.
	return core.FirstNonNil(
		types,
		func(t *checker.Type) *ast.Symbol {
			symbol := t.Symbol()
			// Don't make a recommended completion for an abstract class.
			if symbol != nil &&
				symbol.Flags&(ast.SymbolFlagsEnumMember|ast.SymbolFlagsEnum|ast.SymbolFlagsClass) != 0 &&
				!isAbstractConstructorSymbol(symbol) {
				return getFirstSymbolInChain(symbol, previousToken, typeChecker)
			}
			return nil
		},
	)
}

func isAbstractConstructorSymbol(symbol *ast.Symbol) bool {
	if symbol.Flags&ast.SymbolFlagsClass != 0 {
		declaration := ast.GetClassLikeDeclarationOfSymbol(symbol)
		return declaration != nil && ast.HasSyntacticModifier(declaration, ast.ModifierFlagsAbstract)
	}
	return false
}

func startsWithQuote(s string) bool {
	r, _ := utf8.DecodeRuneInString(s)
	return r == '"' || r == '\''
}

func getClosestSymbolDeclaration(contextToken *ast.Node, location *ast.Node) *ast.Declaration {
	if contextToken == nil {
		return nil
	}

	closestDeclaration := ast.FindAncestorOrQuit(contextToken, func(node *ast.Node) ast.FindAncestorResult {
		if ast.IsFunctionBlock(node) || isArrowFunctionBody(node) || ast.IsBindingPattern(node) {
			return ast.FindAncestorQuit
		}

		if (ast.IsParameterDeclaration(node) || ast.IsTypeParameterDeclaration(node)) &&
			!ast.IsIndexSignatureDeclaration(node.Parent) {
			return ast.FindAncestorTrue
		}
		return ast.FindAncestorFalse
	})

	if closestDeclaration == nil {
		closestDeclaration = ast.FindAncestorOrQuit(location, func(node *ast.Node) ast.FindAncestorResult {
			if ast.IsFunctionBlock(node) || isArrowFunctionBody(node) || ast.IsBindingPattern(node) {
				return ast.FindAncestorQuit
			}

			if ast.IsVariableDeclaration(node) {
				return ast.FindAncestorTrue
			}
			return ast.FindAncestorFalse
		})
	}
	return closestDeclaration
}

func isArrowFunctionBody(node *ast.Node) bool {
	return node.Parent != nil && ast.IsArrowFunction(node.Parent) &&
		(node.Parent.Body() == node ||
			// const a = () => /**/;
			node.Kind == ast.KindEqualsGreaterThanToken)
}

func isInTypeParameterDefault(contextToken *ast.Node) bool {
	if contextToken == nil {
		return false
	}

	node := contextToken
	parent := contextToken.Parent
	for parent != nil {
		if ast.IsTypeParameterDeclaration(parent) {
			return parent.AsTypeParameterDeclaration().DefaultType == node || node.Kind == ast.KindEqualsToken
		}
		node = parent
		parent = parent.Parent
	}

	return false
}

func isDeprecated(symbol *ast.Symbol, typeChecker *checker.Checker) bool {
	declarations := checker.SkipAlias(symbol, typeChecker).Declarations
	return len(declarations) > 0 && core.Every(declarations, func(decl *ast.Declaration) bool { return typeChecker.IsDeprecatedDeclaration(decl) })
}

func (l *LanguageService) getReplacementRangeForContextToken(file *ast.SourceFile, contextToken *ast.Node, position int) *lsproto.Range {
	if contextToken == nil {
		return nil
	}

	// !!! ensure range is single line
	switch contextToken.Kind {
	case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral:
		return l.createRangeFromStringLiteralLikeContent(file, contextToken, position)
	default:
		return new(l.createLspRangeFromNode(contextToken, file))
	}
}

func (l *LanguageService) createRangeFromStringLiteralLikeContent(file *ast.SourceFile, node *ast.StringLiteralLike, position int) *lsproto.Range {
	replacementEnd := node.End() - 1
	nodeStart := astnav.GetStartOfNode(node, file, false /*includeJSDoc*/)
	if ast.IsUnterminatedLiteral(node) {
		// we return no replacement range only if unterminated string is empty
		if nodeStart == replacementEnd {
			return nil
		}
		replacementEnd = min(position, node.End())
	}
	return new(l.createLspRangeFromBounds(nodeStart+1, replacementEnd, file))
}

func quotePropertyName(file *ast.SourceFile, preferences lsutil.UserPreferences, name string) string {
	r, _ := utf8.DecodeRuneInString(name)
	if unicode.IsDigit(r) {
		return name
	}
	return quote(file, preferences, name)
}

// Checks whether type is `string & {}`, which is semantically equivalent to string but
// is not reduced by the checker as a special case used for supporting string literal completions
// for string type.
func isStringAndEmptyAnonymousObjectIntersection(typeChecker *checker.Checker, t *checker.Type) bool {
	if !t.IsIntersection() {
		return false
	}

	return len(t.Types()) == 2 &&
		(areIntersectedTypesAvoidingStringReduction(typeChecker, t.Types()[0], t.Types()[1]) ||
			areIntersectedTypesAvoidingStringReduction(typeChecker, t.Types()[1], t.Types()[0]))
}

func areIntersectedTypesAvoidingStringReduction(typeChecker *checker.Checker, t1 *checker.Type, t2 *checker.Type) bool {
	return t1.IsString() && typeChecker.IsEmptyAnonymousObjectType(t2)
}

func escapeSnippetText(text string) string {
	return strings.ReplaceAll(text, `$`, `\$`)
}

func isNamedImportsOrExports(node *ast.Node) bool {
	return ast.IsNamedImports(node) || ast.IsNamedExports(node)
}

func generateIdentifierForArbitraryString(text string) string {
	needsUnderscore := false
	var identifier strings.Builder
	var ch rune
	var size int

	// Convert "(example, text)" into "_example_text_"
	for pos := 0; pos < len(text); pos += size {
		ch, size = utf8.DecodeRuneInString(text[pos:])
		var validChar bool
		if pos == 0 {
			validChar = scanner.IsIdentifierStart(ch)
		} else {
			validChar = scanner.IsIdentifierPart(ch)
		}
		if size > 0 && validChar {
			if needsUnderscore {
				identifier.WriteRune('_')
			}
			identifier.WriteRune(ch)
			needsUnderscore = false
		} else {
			needsUnderscore = true
		}
	}

	if needsUnderscore {
		identifier.WriteRune('_')
	}

	// Default to "_" if the provided text was empty
	id := identifier.String()
	if id == "" {
		return "_"
	}

	return id
}

// Copied from vscode TS extension.
func getCompletionsSymbolKind(kind lsutil.ScriptElementKind) lsproto.CompletionItemKind {
	switch kind {
	case lsutil.ScriptElementKindPrimitiveType, lsutil.ScriptElementKindKeyword:
		return lsproto.CompletionItemKindKeyword
	case lsutil.ScriptElementKindConstElement, lsutil.ScriptElementKindLetElement, lsutil.ScriptElementKindVariableElement,
		lsutil.ScriptElementKindLocalVariableElement, lsutil.ScriptElementKindAlias, lsutil.ScriptElementKindParameterElement:
		return lsproto.CompletionItemKindVariable

	case lsutil.ScriptElementKindMemberVariableElement, lsutil.ScriptElementKindMemberGetAccessorElement,
		lsutil.ScriptElementKindMemberSetAccessorElement:
		return lsproto.CompletionItemKindField

	case lsutil.ScriptElementKindFunctionElement, lsutil.ScriptElementKindLocalFunctionElement:
		return lsproto.CompletionItemKindFunction

	case lsutil.ScriptElementKindMemberFunctionElement, lsutil.ScriptElementKindConstructSignatureElement,
		lsutil.ScriptElementKindCallSignatureElement, lsutil.ScriptElementKindIndexSignatureElement:
		return lsproto.CompletionItemKindMethod

	case lsutil.ScriptElementKindEnumElement:
		return lsproto.CompletionItemKindEnum

	case lsutil.ScriptElementKindEnumMemberElement:
		return lsproto.CompletionItemKindEnumMember

	case lsutil.ScriptElementKindModuleElement, lsutil.ScriptElementKindExternalModuleName:
		return lsproto.CompletionItemKindModule

	case lsutil.ScriptElementKindClassElement, lsutil.ScriptElementKindTypeElement:
		return lsproto.CompletionItemKindClass

	case lsutil.ScriptElementKindInterfaceElement:
		return lsproto.CompletionItemKindInterface

	case lsutil.ScriptElementKindWarning:
		return lsproto.CompletionItemKindText

	case lsutil.ScriptElementKindScriptElement:
		return lsproto.CompletionItemKindFile

	case lsutil.ScriptElementKindDirectory:
		return lsproto.CompletionItemKindFolder

	case lsutil.ScriptElementKindString:
		return lsproto.CompletionItemKindConstant

	default:
		return lsproto.CompletionItemKindProperty
	}
}

// Editors will use the `sortText` and then fall back to `name` for sorting, but leave ties in response order.
// So, it's important that we sort those ties in the order we want them displayed if it matters. We don't
// strictly need to sort by name or SortText here since clients are going to do it anyway, but we have to
// do the work of comparing them so we can sort those ties appropriately.
func CompareCompletionEntries(a, b *lsproto.CompletionItem) int {
	compareStrings := stringutil.CompareStringsCaseInsensitiveThenSensitive
	result := compareStrings(*a.SortText, *b.SortText)
	if result == stringutil.ComparisonEqual {
		result = compareStrings(a.Label, b.Label)
	}
	return result
}

var (
	keywordCompletionsCache = collections.SyncMap[KeywordCompletionFilters, []*lsproto.CompletionItem]{}
	allKeywordCompletions   = sync.OnceValue(func() []*lsproto.CompletionItem {
		result := make([]*lsproto.CompletionItem, 0, ast.KindLastKeyword-ast.KindFirstKeyword+1)
		for i := ast.KindFirstKeyword; i <= ast.KindLastKeyword; i++ {
			result = append(result, &lsproto.CompletionItem{
				Label:    scanner.TokenToString(i),
				Kind:     new(lsproto.CompletionItemKindKeyword),
				SortText: new(string(SortTextGlobalsOrKeywords)),
			})
		}
		return result
	})
)

func cloneItems(items []*lsproto.CompletionItem) []*CompletionItem {
	if items == nil {
		return nil
	}
	entries := make([]*CompletionItem, len(items))
	for i, item := range items {
		itemClone := *item
		entries[i] = &CompletionItem{CompletionItem: &itemClone}
	}
	return entries
}

func getKeywordCompletions(keywordFilter KeywordCompletionFilters, filterOutTsOnlyKeywords bool) []*CompletionItem {
	if !filterOutTsOnlyKeywords {
		return cloneItems(getTypescriptKeywordCompletions(keywordFilter))
	}

	index := keywordFilter + KeywordCompletionFiltersLast + 1
	if cached, ok := keywordCompletionsCache.Load(index); ok {
		return cloneItems(cached)
	}
	result := core.Filter(
		getTypescriptKeywordCompletions(keywordFilter),
		func(ci *lsproto.CompletionItem) bool {
			return !isTypeScriptOnlyKeyword(scanner.StringToToken(ci.Label))
		},
	)
	keywordCompletionsCache.Store(index, result)
	return cloneItems(result)
}

func getTypescriptKeywordCompletions(keywordFilter KeywordCompletionFilters) []*lsproto.CompletionItem {
	if cached, ok := keywordCompletionsCache.Load(keywordFilter); ok {
		return cached
	}
	result := core.Filter(allKeywordCompletions(), func(entry *lsproto.CompletionItem) bool {
		kind := scanner.StringToToken(entry.Label)
		switch keywordFilter {
		case KeywordCompletionFiltersNone:
			return false
		case KeywordCompletionFiltersAll:
			return isFunctionLikeBodyKeyword(kind) ||
				kind == ast.KindDeclareKeyword ||
				kind == ast.KindModuleKeyword ||
				kind == ast.KindTypeKeyword ||
				kind == ast.KindNamespaceKeyword ||
				kind == ast.KindAbstractKeyword ||
				isTypeKeyword(kind) && kind != ast.KindUndefinedKeyword
		case KeywordCompletionFiltersFunctionLikeBodyKeywords:
			return isFunctionLikeBodyKeyword(kind)
		case KeywordCompletionFiltersClassElementKeywords:
			return isClassMemberCompletionKeyword(kind)
		case KeywordCompletionFiltersInterfaceElementKeywords:
			return isInterfaceOrTypeLiteralCompletionKeyword(kind)
		case KeywordCompletionFiltersConstructorParameterKeywords:
			return ast.IsParameterPropertyModifier(kind)
		case KeywordCompletionFiltersTypeAssertionKeywords:
			return isTypeKeyword(kind) || kind == ast.KindConstKeyword
		case KeywordCompletionFiltersTypeKeywords:
			return isTypeKeyword(kind)
		case KeywordCompletionFiltersTypeKeyword:
			return kind == ast.KindTypeKeyword
		default:
			panic(fmt.Sprintf("Unknown keyword filter: %v", keywordFilter))
		}
	})

	keywordCompletionsCache.Store(keywordFilter, result)
	return result
}

func isTypeScriptOnlyKeyword(kind ast.Kind) bool {
	switch kind {
	case ast.KindAbstractKeyword,
		ast.KindAnyKeyword,
		ast.KindBigIntKeyword,
		ast.KindBooleanKeyword,
		ast.KindDeclareKeyword,
		ast.KindEnumKeyword,
		ast.KindGlobalKeyword,
		ast.KindImplementsKeyword,
		ast.KindInferKeyword,
		ast.KindInterfaceKeyword,
		ast.KindIsKeyword,
		ast.KindKeyOfKeyword,
		ast.KindModuleKeyword,
		ast.KindNamespaceKeyword,
		ast.KindNeverKeyword,
		ast.KindNumberKeyword,
		ast.KindObjectKeyword,
		ast.KindOverrideKeyword,
		ast.KindPrivateKeyword,
		ast.KindProtectedKeyword,
		ast.KindPublicKeyword,
		ast.KindReadonlyKeyword,
		ast.KindStringKeyword,
		ast.KindSymbolKeyword,
		ast.KindTypeKeyword,
		ast.KindUniqueKeyword,
		ast.KindUnknownKeyword:
		return true
	default:
		return false
	}
}

func isFunctionLikeBodyKeyword(kind ast.Kind) bool {
	return kind == ast.KindAsyncKeyword ||
		kind == ast.KindAwaitKeyword ||
		kind == ast.KindUsingKeyword ||
		kind == ast.KindAsKeyword ||
		kind == ast.KindSatisfiesKeyword ||
		kind == ast.KindTypeKeyword ||
		!ast.IsContextualKeyword(kind) && !isClassMemberCompletionKeyword(kind)
}

func isClassMemberCompletionKeyword(kind ast.Kind) bool {
	switch kind {
	case ast.KindAbstractKeyword, ast.KindAccessorKeyword, ast.KindConstructorKeyword, ast.KindGetKeyword,
		ast.KindSetKeyword, ast.KindAsyncKeyword, ast.KindDeclareKeyword, ast.KindOverrideKeyword:
		return true
	default:
		return ast.IsClassMemberModifier(kind)
	}
}

func isInterfaceOrTypeLiteralCompletionKeyword(kind ast.Kind) bool {
	return kind == ast.KindReadonlyKeyword
}

func isContextualKeywordInAutoImportableExpressionSpace(keyword string) bool {
	return keyword == "abstract" ||
		keyword == "async" ||
		keyword == "await" ||
		keyword == "declare" ||
		keyword == "module" ||
		keyword == "namespace" ||
		keyword == "type" ||
		keyword == "satisfies" ||
		keyword == "as"
}

func getContextualKeywords(file *ast.SourceFile, contextToken *ast.Node, position int) []*lsproto.CompletionItem {
	var entries []*lsproto.CompletionItem
	// An `AssertClause` can come after an import declaration:
	//  import * from "foo" |
	//  import "foo" |
	// or after a re-export declaration that has a module specifier:
	//  export { foo } from "foo" |
	// Source: https://tc39.es/proposal-import-assertions/
	if contextToken != nil {
		parent := contextToken.Parent
		tokenLine := scanner.GetECMALineOfPosition(file, contextToken.End())
		currentLine := scanner.GetECMALineOfPosition(file, position)
		if (ast.IsImportDeclaration(parent) ||
			ast.IsExportDeclaration(parent) && parent.ModuleSpecifier() != nil) &&
			contextToken == parent.ModuleSpecifier() &&
			tokenLine == currentLine {
			entries = append(entries, &lsproto.CompletionItem{
				Label:    scanner.TokenToString(ast.KindAssertKeyword),
				Kind:     new(lsproto.CompletionItemKindKeyword),
				SortText: new(string(SortTextGlobalsOrKeywords)),
			})
		}
	}
	return entries
}

func (l *LanguageService) getJSCompletionEntries(
	ctx context.Context,
	file *ast.SourceFile,
	position int,
	uniqueNames *collections.Set[string],
	sortedEntries []*CompletionItem,
) []*CompletionItem {
	nameTable := file.GetNameTable()
	for name, pos := range nameTable {
		// Skip identifiers produced only from the current location
		if pos == position {
			continue
		}
		if !uniqueNames.Has(name) && scanner.IsIdentifierText(name, core.LanguageVariantStandard) {
			uniqueNames.Add(name)
			sortedEntries = append(sortedEntries, &CompletionItem{
				CompletionItem: &lsproto.CompletionItem{
					Label:            name,
					Kind:             new(lsproto.CompletionItemKindText),
					SortText:         new(string(SortTextJavascriptIdentifiers)),
					CommitCharacters: new([]string{}),
				},
			})
		}
	}
	return sortedEntries
}

func (l *LanguageService) getOptionalReplacementSpan(location *ast.Node, file *ast.SourceFile) *lsproto.Range {
	// StringLiteralLike locations are handled separately in stringCompletions.ts
	if location != nil && (location.Kind == ast.KindIdentifier || location.Kind == ast.KindPrivateIdentifier) {
		start := astnav.GetStartOfNode(location, file, false /*includeJSDoc*/)
		return new(l.createLspRangeFromBounds(start, location.End(), file))
	}
	return nil
}

func isMemberCompletionKind(kind CompletionKind) bool {
	return kind == CompletionKindObjectPropertyDeclaration ||
		kind == CompletionKindMemberLike ||
		kind == CompletionKindPropertyAccess
}

func tryGetFunctionLikeBodyCompletionContainer(contextToken *ast.Node) *ast.Node {
	if contextToken == nil {
		return nil
	}

	var prev *ast.Node
	container := ast.FindAncestorOrQuit(contextToken, func(node *ast.Node) ast.FindAncestorResult {
		if ast.IsClassLike(node) {
			return ast.FindAncestorQuit
		}
		if ast.IsFunctionLikeDeclaration(node) && prev == node.Body() {
			return ast.FindAncestorTrue
		}
		prev = node
		return ast.FindAncestorFalse
	})
	return container
}

func computeCommitCharactersAndIsNewIdentifier(
	contextToken *ast.Node,
	file *ast.SourceFile,
	position int,
) (isNewIdentifierLocation bool, defaultCommitCharacters []string) {
	if contextToken == nil {
		return false, allCommitCharacters
	}
	containingNodeKind := contextToken.Parent.Kind
	tokenKind := keywordForNode(contextToken)
	// Previous token may have been a keyword that was converted to an identifier.
	switch tokenKind {
	case ast.KindCommaToken:
		switch containingNodeKind {
		// func( a, |
		// new C(a, |
		case ast.KindCallExpression, ast.KindNewExpression:
			expression := contextToken.Parent.Expression()
			// func\n(a, |
			if getLineOfPosition(file, expression.End()) != getLineOfPosition(file, position) {
				return true, noCommaCommitCharacters
			}
			return true, allCommitCharacters
		// const x = (a, |
		case ast.KindBinaryExpression:
			return true, noCommaCommitCharacters
		// constructor( a, | /* public, protected, private keywords are allowed here, so show completion */
		// var x: (s: string, list|
		// const obj = { x, |
		case ast.KindConstructor, ast.KindFunctionType, ast.KindObjectLiteralExpression:
			return true, emptyCommitCharacters
		// [a, |
		case ast.KindArrayLiteralExpression:
			return true, allCommitCharacters
		default:
			return false, allCommitCharacters
		}
	case ast.KindOpenParenToken:
		switch containingNodeKind {
		// func( |
		// new C(a|
		case ast.KindCallExpression, ast.KindNewExpression:
			expression := contextToken.Parent.Expression()
			// func\n( |
			if getLineOfPosition(file, expression.End()) != getLineOfPosition(file, position) {
				return true, noCommaCommitCharacters
			}
			return true, allCommitCharacters
		// const x = (a|
		case ast.KindParenthesizedExpression:
			return true, noCommaCommitCharacters
		// constructor( |
		// function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */
		case ast.KindConstructor, ast.KindParenthesizedType:
			return true, emptyCommitCharacters
		default:
			return false, allCommitCharacters
		}
	case ast.KindOpenBracketToken:
		switch containingNodeKind {
		// [ |
		// [ | : string ]
		// [ | : string ]
		// [ |    /* this can become an index signature */
		case ast.KindArrayLiteralExpression, ast.KindIndexSignature, ast.KindTupleType, ast.KindComputedPropertyName:
			return true, allCommitCharacters
		default:
			return false, allCommitCharacters
		}
	// module |
	// namespace |
	// import |
	case ast.KindModuleKeyword, ast.KindNamespaceKeyword, ast.KindImportKeyword:
		return true, emptyCommitCharacters
	case ast.KindDotToken:
		switch containingNodeKind {
		// module A.|
		case ast.KindModuleDeclaration:
			return true, emptyCommitCharacters
		default:
			return false, allCommitCharacters
		}
	case ast.KindOpenBraceToken:
		switch containingNodeKind {
		// class A { |
		// const obj = { |
		case ast.KindClassDeclaration, ast.KindObjectLiteralExpression:
			return true, emptyCommitCharacters
		default:
			return false, allCommitCharacters
		}
	case ast.KindEqualsToken:
		switch containingNodeKind {
		// const x = a|
		// x = a|
		case ast.KindVariableDeclaration, ast.KindBinaryExpression:
			return true, allCommitCharacters
		default:
			return false, allCommitCharacters
		}
	case ast.KindTemplateHead:
		// `aa ${|
		return containingNodeKind == ast.KindTemplateExpression, allCommitCharacters
	case ast.KindTemplateMiddle:
		// `aa ${10} dd ${|
		return containingNodeKind == ast.KindTemplateSpan, allCommitCharacters
	case ast.KindAsyncKeyword:
		// const obj = { async c|()
		// const obj = { async c|
		if containingNodeKind == ast.KindMethodDeclaration || containingNodeKind == ast.KindShorthandPropertyAssignment {
			return true, emptyCommitCharacters
		}
		return false, allCommitCharacters
	case ast.KindAsteriskToken:
		// const obj = { * c|
		if containingNodeKind == ast.KindMethodDeclaration {
			return true, emptyCommitCharacters
		}
		return false, allCommitCharacters
	}

	if isClassMemberCompletionKeyword(tokenKind) {
		return true, emptyCommitCharacters
	}

	return false, allCommitCharacters
}

func keywordForNode(node *ast.Node) ast.Kind {
	if ast.IsIdentifier(node) {
		return scanner.IdentifierToKeywordKind(node.AsIdentifier())
	}
	return node.Kind
}

// Finds the first node that "embraces" the position, so that one may
// accurately aggregate locals from the closest containing scope.
func getScopeNode(initialToken *ast.Node, position int, file *ast.SourceFile) *ast.Node {
	scope := initialToken
	for scope != nil && !positionBelongsToNode(scope, position, file) {
		scope = scope.Parent
	}
	return scope
}

func isSnippetScope(scopeNode *ast.Node) bool {
	switch scopeNode.Kind {
	case ast.KindSourceFile,
		ast.KindTemplateExpression,
		ast.KindJsxExpression,
		ast.KindBlock:
		return true
	default:
		return ast.IsStatement(scopeNode)
	}
}

// Determines if a type is exactly the same type resolved by the global 'self', 'global', or 'globalThis'.
func isProbablyGlobalType(t *checker.Type, file *ast.SourceFile, typeChecker *checker.Checker) bool {
	// The type of `self` and `window` is the same in lib.dom.d.ts, but `window` does not exist in
	// lib.webworker.d.ts, so checking against `self` is also a check against `window` when it exists.
	selfSymbol := typeChecker.GetGlobalSymbol("self", ast.SymbolFlagsValue, nil /*diagnostic*/)
	if selfSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(selfSymbol, file.AsNode()) == t {
		return true
	}
	globalSymbol := typeChecker.GetGlobalSymbol("global", ast.SymbolFlagsValue, nil /*diagnostic*/)
	if globalSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(globalSymbol, file.AsNode()) == t {
		return true
	}
	globalThisSymbol := typeChecker.GetGlobalSymbol("globalThis", ast.SymbolFlagsValue, nil /*diagnostic*/)
	if globalThisSymbol != nil && typeChecker.GetTypeOfSymbolAtLocation(globalThisSymbol, file.AsNode()) == t {
		return true
	}
	return false
}

func tryGetTypeLiteralNode(node *ast.Node) *ast.TypeLiteralNodeNode {
	if node == nil {
		return nil
	}

	parent := node.Parent
	switch node.Kind {
	case ast.KindOpenBraceToken:
		if ast.IsTypeLiteralNode(parent) {
			return parent
		}
	case ast.KindSemicolonToken, ast.KindCommaToken, ast.KindIdentifier:
		if parent.Kind == ast.KindPropertySignature && ast.IsTypeLiteralNode(parent.Parent) {
			return parent.Parent
		}
	}

	return nil
}

func getConstraintOfTypeArgumentProperty(node *ast.Node, typeChecker *checker.Checker) *checker.Type {
	if node == nil {
		return nil
	}

	if ast.IsTypeNode(node) {
		constraint := typeChecker.GetTypeArgumentConstraint(node)
		if constraint != nil {
			return constraint
		}
	}

	t := getConstraintOfTypeArgumentProperty(node.Parent, typeChecker)
	if t == nil {
		return nil
	}

	switch node.Kind {
	case ast.KindPropertySignature:
		// Try to get the reparsed node first - we may be in JSDoc.
		reparsed := ast.GetReparsedNodeForNode(node)
		if symbol := reparsed.Symbol(); symbol != nil {
			return typeChecker.GetTypeOfPropertyOfContextualType(t, symbol.Name)
		}

		// In some cases, we won't have a corresponding symbol
		// (e.g. JSDoc types that never get re-attached) so we'll use
		// the name as declared by the property as a best-effort.
		if name, ok := ast.TryGetTextOfPropertyName(reparsed.Name()); ok {
			return typeChecker.GetTypeOfPropertyOfContextualType(t, name)
		}

		return nil
	case ast.KindColonToken:
		if node.Parent.Kind == ast.KindPropertySignature {
			// The cursor is at a property value location like `Foo<{ x: | }`.
			// `t` already refers to the appropriate property type.
			return t
		}
	case ast.KindIntersectionType, ast.KindTypeLiteral, ast.KindUnionType:
		return t
	case ast.KindOpenBracketToken:
		return typeChecker.GetElementTypeOfArrayType(t)
	}

	return nil
}

func tryGetObjectLikeCompletionContainer(contextToken *ast.Node, position int, file *ast.SourceFile) *ast.ObjectLiteralLike {
	if contextToken == nil {
		return nil
	}

	parent := contextToken.Parent
	switch contextToken.Kind {
	// const x = { |
	// const x = { a: 0, |
	case ast.KindOpenBraceToken, ast.KindCommaToken:
		if ast.IsObjectLiteralExpression(parent) || ast.IsObjectBindingPattern(parent) {
			return parent
		}
	case ast.KindAsteriskToken:
		if ast.IsMethodDeclaration(parent) && ast.IsObjectLiteralExpression(parent.Parent) {
			return parent.Parent
		}
	case ast.KindAsyncKeyword:
		if ast.IsObjectLiteralExpression(parent.Parent) {
			return parent.Parent
		}
	case ast.KindIdentifier:
		if contextToken.Text() == "async" && ast.IsShorthandPropertyAssignment(parent) {
			return parent.Parent
		} else {
			if ast.IsObjectLiteralExpression(parent.Parent) &&
				(ast.IsSpreadAssignment(parent) ||
					ast.IsShorthandPropertyAssignment(parent) &&
						getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position)) {
				return parent.Parent
			}
			ancestorNode := ast.FindAncestor(parent, ast.IsPropertyAssignment)
			if ancestorNode != nil && lsutil.GetLastToken(ancestorNode, file) == contextToken && ast.IsObjectLiteralExpression(ancestorNode.Parent) {
				return ancestorNode.Parent
			}
		}
	default:
		if parent.Parent != nil && parent.Parent.Parent != nil &&
			(ast.IsMethodDeclaration(parent.Parent) ||
				ast.IsGetAccessorDeclaration(parent.Parent) ||
				ast.IsSetAccessorDeclaration(parent.Parent)) &&
			ast.IsObjectLiteralExpression(parent.Parent.Parent) {
			return parent.Parent.Parent
		}
		if ast.IsSpreadAssignment(parent) && ast.IsObjectLiteralExpression(parent.Parent) {
			return parent.Parent
		}
		ancestorNode := ast.FindAncestor(parent, ast.IsPropertyAssignment)
		if contextToken.Kind != ast.KindColonToken &&
			ancestorNode != nil && lsutil.GetLastToken(ancestorNode, file) == contextToken &&
			ast.IsObjectLiteralExpression(ancestorNode.Parent) {
			return ancestorNode.Parent
		}
	}

	return nil
}

func tryGetObjectLiteralContextualType(node *ast.ObjectLiteralExpressionNode, typeChecker *checker.Checker) *checker.Type {
	t := typeChecker.GetContextualType(node, checker.ContextFlagsNone)
	if t != nil {
		return t
	}

	parent := ast.WalkUpParenthesizedExpressions(node.Parent)
	if ast.IsBinaryExpression(parent) &&
		parent.AsBinaryExpression().OperatorToken.Kind == ast.KindEqualsToken &&
		node == parent.AsBinaryExpression().Left {
		// Object literal is assignment pattern: ({ | } = x)
		return typeChecker.GetTypeAtLocation(parent)
	}
	if ast.IsExpression(parent) {
		// f(() => (({ | })));
		return typeChecker.GetContextualType(parent, checker.ContextFlagsNone)
	}

	return nil
}

func getPropertiesForObjectExpression(
	contextualType *checker.Type,
	completionsType *checker.Type,
	obj *ast.Node,
	typeChecker *checker.Checker,
) []*ast.Symbol {
	hasCompletionsType := completionsType != nil && completionsType != contextualType
	var types []*checker.Type
	if contextualType.IsUnion() {
		types = contextualType.Types()
	} else {
		types = []*checker.Type{contextualType}
	}
	promiseFilteredContextualType := typeChecker.GetUnionType(core.Filter(types, func(t *checker.Type) bool {
		return typeChecker.GetPromisedTypeOfPromise(t) == nil
	}))

	var t *checker.Type
	if hasCompletionsType && completionsType.Flags()&checker.TypeFlagsAnyOrUnknown == 0 {
		t = typeChecker.GetUnionType([]*checker.Type{promiseFilteredContextualType, completionsType})
	} else {
		t = promiseFilteredContextualType
	}

	// Filter out members whose only declaration is the object literal itself to avoid
	// self-fulfilling completions like:
	//
	// function f<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.PropertyName() != nil {
			// include only identifiers in completion list
			if member.PropertyName().Kind == ast.KindIdentifier {
				existingName = member.PropertyName().Text()
			}
		} else {
			// TODO: Account for computed property name
			// NOTE: if one only performs this step when m.name is an identifier,
			// things like '__proto__' are not filtered out.
			name := ast.GetNameOfDeclaration(member)
			if name != nil && ast.IsPropertyNameLiteral(name) {
				existingName = name.Text()
			}
		}

		if existingName != "" {
			existingMemberNames.Add(existingName)
		}
	}

	filteredSymbols := core.Filter(contextualMemberSymbols, func(m *ast.Symbol) bool {
		return !existingMemberNames.Has(m.Name)
	})

	return filteredSymbols, membersDeclaredBySpreadAssignment
}

func isCurrentlyEditingNode(node *ast.Node, file *ast.SourceFile, position int) bool {
	start := astnav.GetStartOfNode(node, file, false /*includeJSDoc*/)
	return start <= position && position <= node.End()
}

func setMemberDeclaredBySpreadAssignment(declaration *ast.Node, members *collections.Set[string], typeChecker *checker.Checker) {
	expression := declaration.Expression()
	symbol := typeChecker.GetSymbolAtLocation(expression)
	var t *checker.Type
	if symbol != nil {
		t = typeChecker.GetTypeOfSymbolAtLocation(symbol, expression)
	}
	var properties []*ast.Symbol
	if t != nil && t.Flags()&checker.TypeFlagsStructuredType != 0 {
		properties = t.AsStructuredType().Properties()
	}
	for _, property := range properties {
		members.Add(property.Name)
	}
}

// Returns the immediate owning class declaration of a context token,
// on the condition that one exists and that the context implies completion should be given.
func tryGetConstructorLikeCompletionContainer(contextToken *ast.Node) *ast.ConstructorDeclarationNode {
	if contextToken == nil {
		return nil
	}

	parent := contextToken.Parent
	switch contextToken.Kind {
	case ast.KindOpenParenToken, ast.KindCommaToken:
		if ast.IsConstructorDeclaration(parent) {
			return parent
		}
		return nil
	default:
		if isConstructorParameterCompletion(contextToken) {
			return parent.Parent
		}
	}
	return nil
}

func isConstructorParameterCompletion(node *ast.Node) bool {
	return node.Parent != nil && ast.IsParameterDeclaration(node.Parent) && ast.IsConstructorDeclaration(node.Parent.Parent) &&
		(ast.IsParameterPropertyModifier(node.Kind) || ast.IsDeclarationName(node))
}

// Returns the immediate owning class declaration of a context token,
// on the condition that one exists and that the context implies completion should be given.
func tryGetObjectTypeDeclarationCompletionContainer(
	file *ast.SourceFile,
	contextToken *ast.Node,
	location *ast.Node,
	position int,
) *ast.ObjectTypeDeclaration {
	// class c { method() { } | method2() { } }
	switch location.Kind {
	case ast.KindSyntaxList:
		if ast.IsObjectTypeDeclaration(location.Parent) {
			return location.Parent
		}
		return nil
	case ast.KindEndOfFile:
		stmtList := location.Parent.StatementList()
		if stmtList != nil && len(stmtList.Nodes) > 0 && ast.IsObjectTypeDeclaration(stmtList.Nodes[len(stmtList.Nodes)-1]) {
			cls := stmtList.Nodes[len(stmtList.Nodes)-1]
			if astnav.FindChildOfKind(cls, ast.KindCloseBraceToken, file) == nil {
				return cls
			}
		}
	case ast.KindPrivateIdentifier:
		if ast.IsPropertyDeclaration(location.Parent) {
			return ast.FindAncestor(location, ast.IsClassLike)
		}
	case ast.KindIdentifier:
		originalKeywordKind := scanner.IdentifierToKeywordKind(location.AsIdentifier())
		if originalKeywordKind != ast.KindUnknown {
			return nil
		}
		// class c { public prop = c| }
		if ast.IsPropertyDeclaration(location.Parent) && location.Parent.Initializer() == location {
			return nil
		}
		// class c extends React.Component { a: () => 1\n compon| }
		if isFromObjectTypeDeclaration(location) {
			return ast.FindAncestor(location, ast.IsObjectTypeDeclaration)
		}
	}

	if contextToken == nil {
		return nil
	}

	// class C { blah; constructor/**/ }
	// or
	// class C { blah \n constructor/**/ }
	if location.Kind == ast.KindConstructorKeyword ||
		(ast.IsIdentifier(contextToken) && ast.IsPropertyDeclaration(contextToken.Parent) && ast.IsClassLike(location)) {
		return ast.FindAncestor(contextToken, ast.IsClassLike)
	}

	switch contextToken.Kind {
	// class c { public prop = | /* global completions */ }
	case ast.KindEqualsToken:
		return nil
	// class c {getValue(): number; | }
	// class c { method() { } | }
	case ast.KindSemicolonToken, ast.KindCloseBraceToken:
		// class c { method() { } b| }
		if isFromObjectTypeDeclaration(location) && location.Parent.Name() == location {
			return location.Parent.Parent
		}
		if ast.IsObjectTypeDeclaration(location) {
			return location
		}
		return nil
	// class c { |
	// class c {getValue(): number, | }
	case ast.KindOpenBraceToken, ast.KindCommaToken:
		if ast.IsObjectTypeDeclaration(contextToken.Parent) {
			return contextToken.Parent
		}
		return nil
	default:
		if ast.IsObjectTypeDeclaration(location) {
			// class C extends React.Component { a: () => 1\n| }
			// class C { prop = ""\n | }
			if getLineOfPosition(file, contextToken.End()) != getLineOfPosition(file, position) {
				return location
			}
			isValidKeyword := core.IfElse(
				ast.IsClassLike(contextToken.Parent.Parent),
				isClassMemberCompletionKeyword,
				isInterfaceOrTypeLiteralCompletionKeyword,
			)

			if isValidKeyword(contextToken.Kind) || contextToken.Kind == ast.KindAsteriskToken ||
				ast.IsIdentifier(contextToken) && isValidKeyword(scanner.IdentifierToKeywordKind(contextToken.AsIdentifier())) {
				return contextToken.Parent.Parent
			}
		}

		return nil
	}
}

func isFromObjectTypeDeclaration(node *ast.Node) bool {
	return node.Parent != nil && ast.IsClassOrTypeElement(node.Parent) && ast.IsObjectTypeDeclaration(node.Parent.Parent)
}

// Filters out completion suggestions for class elements.
func filterClassMembersList(
	baseSymbols []*ast.Symbol,
	existingMembers []*ast.ClassElement,
	classElementModifierFlags ast.ModifierFlags,
	file *ast.SourceFile,
	position int,
) []*ast.Symbol {
	existingMemberNames := collections.Set[string]{}
	for _, member := range existingMembers {
		// Ignore omitted expressions for missing members.
		if member.Kind != ast.KindPropertyDeclaration &&
			member.Kind != ast.KindMethodDeclaration &&
			member.Kind != ast.KindGetAccessor &&
			member.Kind != ast.KindSetAccessor {
			continue
		}

		// If this is the current item we are editing right now, do not filter it out
		if isCurrentlyEditingNode(member, file, position) {
			continue
		}

		// Don't filter member even if the name matches if it is declared private in the list.
		if member.ModifierFlags()&ast.ModifierFlagsPrivate != 0 {
			continue
		}

		// Do not filter it out if the static presence doesn't match.
		if ast.IsStatic(member) != (classElementModifierFlags&ast.ModifierFlagsStatic != 0) {
			continue
		}

		existingName := ast.GetPropertyNameForPropertyNameNode(member.Name())
		if existingName != "" {
			existingMemberNames.Add(existingName)
		}
	}

	return core.Filter(baseSymbols, func(propertySymbol *ast.Symbol) bool {
		return !existingMemberNames.Has(ast.SymbolName(propertySymbol)) &&
			len(propertySymbol.Declarations) > 0 &&
			checker.GetDeclarationModifierFlagsFromSymbol(propertySymbol)&ast.ModifierFlagsPrivate == 0 &&
			!(propertySymbol.ValueDeclaration != nil && ast.IsPrivateIdentifierClassElementDeclaration(propertySymbol.ValueDeclaration))
	})
}

func tryGetContainingJsxElement(contextToken *ast.Node, file *ast.SourceFile) *ast.JsxOpeningLikeElement {
	if contextToken == nil {
		return nil
	}

	parent := contextToken.Parent
	switch contextToken.Kind {
	case ast.KindGreaterThanToken, ast.KindLessThanSlashToken, ast.KindSlashToken, ast.KindIdentifier,
		ast.KindPropertyAccessExpression, ast.KindJsxNamespacedName, ast.KindJsxAttributes, ast.KindJsxAttribute, ast.KindJsxSpreadAttribute:
		if parent != nil && (parent.Kind == ast.KindJsxSelfClosingElement || parent.Kind == ast.KindJsxOpeningElement) {
			if contextToken.Kind == ast.KindGreaterThanToken {
				precedingToken := astnav.FindPrecedingToken(file, contextToken.Pos())
				if len(parent.TypeArguments()) == 0 ||
					precedingToken != nil && precedingToken.Kind == ast.KindSlashToken {
					return nil
				}
			}
			return parent
		} else if parent != nil && ast.IsJsxNamespacedName(parent) &&
			parent.Parent != nil && (parent.Parent.Kind == ast.KindJsxSelfClosingElement || parent.Parent.Kind == ast.KindJsxOpeningElement) {
			return parent.Parent
		} else if parent != nil && parent.Kind == ast.KindJsxAttribute {
			// Currently we parse JsxOpeningLikeElement as:
			//      JsxOpeningLikeElement
			//          attributes: JsxAttributes
			//             properties: NodeArray<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(
	ctx context.Context,
	position int,
	file *ast.SourceFile,
	items []*CompletionItem,
	defaultCommitCharacters *[]string,
	optionalReplacementSpan *lsproto.Range,
) *lsproto.CompletionItemDefaults {
	var itemDefaults *lsproto.CompletionItemDefaults
	if defaultCommitCharacters != nil {
		supportsItemCommitCharacters := clientSupportsItemCommitCharacters(ctx)
		if clientSupportsDefaultCommitCharacters(ctx) && supportsItemCommitCharacters {
			itemDefaults = &lsproto.CompletionItemDefaults{
				CommitCharacters: defaultCommitCharacters,
			}
		} else if supportsItemCommitCharacters {
			for _, item := range items {
				if item.CommitCharacters == nil {
					item.CommitCharacters = defaultCommitCharacters
				}
			}
		}
	}
	if optionalReplacementSpan != nil {
		// Ported from vscode ts extension.
		insertRange := lsproto.Range{
			Start: optionalReplacementSpan.Start,
			End:   l.createLspPosition(position, file),
		}
		if clientSupportsDefaultEditRange(ctx) {
			itemDefaults = core.OrElse(itemDefaults, &lsproto.CompletionItemDefaults{})
			itemDefaults.EditRange = &lsproto.RangeOrEditRangeWithInsertReplace{
				EditRangeWithInsertReplace: &lsproto.EditRangeWithInsertReplace{
					Insert:  insertRange,
					Replace: *optionalReplacementSpan,
				},
			}
			for _, item := range items {
				// If `editRange` is set, `insertText` is ignored by the client, so we need to
				// provide `textEdit` instead.
				if item.InsertText != nil && item.TextEdit == nil {
					item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{
						InsertReplaceEdit: &lsproto.InsertReplaceEdit{
							NewText: *item.InsertText,
							Insert:  insertRange,
							Replace: *optionalReplacementSpan,
						},
					}
					item.InsertText = nil
				}
			}
		} else if clientSupportsItemInsertReplace(ctx) {
			for _, item := range items {
				if item.TextEdit == nil {
					item.TextEdit = &lsproto.TextEditOrInsertReplaceEdit{
						InsertReplaceEdit: &lsproto.InsertReplaceEdit{
							NewText: *core.OrElse(item.InsertText, &item.Label),
							Insert:  insertRange,
							Replace: *optionalReplacementSpan,
						},
					}
				}
			}
		}
	}

	return itemDefaults
}

func (l *LanguageService) specificKeywordCompletionInfo(
	ctx context.Context,
	position int,
	file *ast.SourceFile,
	items []*CompletionItem,
	isNewIdentifierLocation bool,
	optionalReplacementSpan *lsproto.Range,
) *CompletionList {
	defaultCommitCharacters := getDefaultCommitCharacters(isNewIdentifierLocation)
	itemDefaults := l.setItemDefaults(
		ctx,
		position,
		file,
		items,
		&defaultCommitCharacters,
		optionalReplacementSpan,
	)
	return &CompletionList{
		IsIncomplete: false,
		ItemDefaults: itemDefaults,
		Items:        items,
	}
}

func (l *LanguageService) getJsxClosingTagCompletion(
	ctx context.Context,
	location *ast.Node,
	file *ast.SourceFile,
	position int,
) *CompletionList {
	// We wanna walk up the tree till we find a JSX closing element.
	jsxClosingElement := ast.FindAncestorOrQuit(location, func(node *ast.Node) ast.FindAncestorResult {
		switch node.Kind {
		case ast.KindJsxClosingElement:
			return ast.FindAncestorTrue
		case ast.KindLessThanSlashToken, ast.KindGreaterThanToken, ast.KindIdentifier, ast.KindPropertyAccessExpression:
			return ast.FindAncestorFalse
		default:
			return ast.FindAncestorQuit
		}
	})

	if jsxClosingElement == nil {
		return nil
	}

	// In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag,
	// instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element.
	// For example:
	//     var x = <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 := astnav.FindChildOfKind(jsxClosingElement, ast.KindGreaterThanToken, file) != nil
	tagName := jsxClosingElement.Parent.AsJsxElement().OpeningElement.TagName()
	closingTag := scanner.GetTextOfNode(tagName)
	fullClosingTag := closingTag + core.IfElse(hasClosingAngleBracket, "", ">")
	optionalReplacementSpan := new(l.createLspRangeFromNode(jsxClosingElement.TagName(), file))
	defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/)

	lspItem := l.createLSPCompletionItem(
		ctx,
		fullClosingTag, /*name*/
		"",             /*insertText*/
		"",             /*filterText*/
		SortTextLocationPriority,
		lsutil.ScriptElementKindClassElement,
		lsutil.ScriptElementKindModifierNone, /*kindModifiers*/
		nil,                                  /*replacementSpan*/
		nil,                                  /*commitCharacters*/
		nil,                                  /*labelDetails*/
		file,
		position,
		true,  /*isMemberCompletion*/
		false, /*isSnippet*/
		false, /*hasAction*/
		false, /*preselect*/
		"",    /*source*/
		nil,   /*autoImportEntryData*/ // !!! jsx autoimports
		nil,   /*detail*/
	)
	item := &CompletionItem{
		CompletionItem: lspItem,
	}
	items := []*CompletionItem{item}
	itemDefaults := l.setItemDefaults(
		ctx,
		position,
		file,
		items,
		&defaultCommitCharacters,
		optionalReplacementSpan,
	)

	return &CompletionList{
		IsIncomplete: false,
		ItemDefaults: itemDefaults,
		Items:        items,
	}
}

func (l *LanguageService) createLSPCompletionItem(
	ctx context.Context,
	name string,
	insertText string,
	filterText string,
	sortText SortText,
	elementKind lsutil.ScriptElementKind,
	kindModifiers lsutil.ScriptElementKindModifier,
	replacementSpan *lsproto.Range,
	commitCharacters *[]string,
	labelDetails *lsproto.CompletionItemLabelDetails,
	file *ast.SourceFile,
	position int,
	isMemberCompletion bool,
	isSnippet bool,
	hasAction bool,
	preselect bool,
	source string,
	autoImportFix *lsproto.AutoImportFix,
	detail *string,
) *lsproto.CompletionItem {
	kind := getCompletionsSymbolKind(elementKind)
	data := &lsproto.CompletionItemData{
		FileName:   file.FileName(),
		Position:   int32(position),
		Source:     source,
		Name:       name,
		AutoImport: autoImportFix,
	}

	// Text edit
	var textEdit *lsproto.TextEditOrInsertReplaceEdit
	if replacementSpan != nil {
		textEdit = &lsproto.TextEditOrInsertReplaceEdit{
			TextEdit: &lsproto.TextEdit{
				NewText: core.IfElse(insertText == "", name, insertText),
				Range:   *replacementSpan,
			},
		}
	}

	// Filter text

	// Ported from vscode ts extension.
	wordSize, wordStart := getWordLengthAndStart(file, position)
	dotAccessor := getDotAccessor(file, position-wordSize)
	if filterText == "" {
		filterText = getFilterText(file, position, insertText, name, wordStart, dotAccessor)
	}

	// Adjustements based on kind modifiers.
	var tags *[]lsproto.CompletionItemTag
	// Copied from vscode ts extension: `MyCompletionItem.constructor`.
	if kindModifiers&lsutil.ScriptElementKindModifierOptional != 0 {
		if insertText == "" {
			insertText = name
		}
		if filterText == "" {
			filterText = name
		}
		name = name + "?"
	}
	if kindModifiers&lsutil.ScriptElementKindModifierDeprecated != 0 {
		tags = &[]lsproto.CompletionItemTag{lsproto.CompletionItemTagDeprecated}
	}

	if hasAction && source != "" {
		// !!! adjust label like vscode does
	}

	// Client assumes plain text by default.
	var insertTextFormat *lsproto.InsertTextFormat
	if isSnippet {
		insertTextFormat = new(lsproto.InsertTextFormatSnippet)
	}

	return &lsproto.CompletionItem{
		Label:            name,
		LabelDetails:     labelDetails,
		Kind:             &kind,
		Tags:             tags,
		Detail:           detail,
		Preselect:        boolToPtr(preselect),
		SortText:         new(string(sortText)),
		FilterText:       strPtrTo(filterText),
		InsertText:       strPtrTo(insertText),
		InsertTextFormat: insertTextFormat,
		TextEdit:         textEdit,
		CommitCharacters: commitCharacters,
		Data:             data,
	}
}

func (l *LanguageService) getLabelCompletionsAtPosition(
	ctx context.Context,
	node *ast.BreakOrContinueStatement,
	file *ast.SourceFile,
	position int,
	optionalReplacementSpan *lsproto.Range,
) *CompletionList {
	items := l.getLabelStatementCompletions(ctx, node, file, position)
	if len(items) == 0 {
		return nil
	}
	defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/)
	itemDefaults := l.setItemDefaults(
		ctx,
		position,
		file,
		items,
		&defaultCommitCharacters,
		optionalReplacementSpan,
	)
	return &CompletionList{
		IsIncomplete: false,
		ItemDefaults: itemDefaults,
		Items:        items,
	}
}

func (l *LanguageService) getLabelStatementCompletions(
	ctx context.Context,
	node *ast.BreakOrContinueStatement,
	file *ast.SourceFile,
	position int,
) []*CompletionItem {
	var uniques collections.Set[string]
	var items []*CompletionItem
	current := node
	for current != nil {
		if ast.IsFunctionLike(current) {
			break
		}
		if ast.IsLabeledStatement(current) {
			name := current.Label().Text()
			if !uniques.Has(name) {
				uniques.Add(name)
				lspItem := l.createLSPCompletionItem(
					ctx,
					name,
					"", /*insertText*/
					"", /*filterText*/
					SortTextLocationPriority,
					lsutil.ScriptElementKindLabel,
					lsutil.ScriptElementKindModifierNone, /*kindModifiers*/
					nil,                                  /*replacementSpan*/
					nil,                                  /*commitCharacters*/
					nil,                                  /*labelDetails*/
					file,
					position,
					false, /*isMemberCompletion*/
					false, /*isSnippet*/
					false, /*hasAction*/
					false, /*preselect*/
					"",    /*source*/
					nil,   /*autoImportEntryData*/
					nil,   /*detail*/
				)
				items = append(items, &CompletionItem{
					CompletionItem: lspItem,
				})
			}
		}
		current = current.Parent
	}
	return items
}

func isCompletionListBlocker(
	contextToken *ast.Node,
	previousToken *ast.Node,
	location *ast.Node,
	file *ast.SourceFile,
	position int,
	typeChecker *checker.Checker,
) bool {
	return isInStringOrRegularExpressionOrTemplateLiteral(contextToken, position) ||
		isSolelyIdentifierDefinitionLocation(contextToken, previousToken, file, position, typeChecker) ||
		isDotOfNumericLiteral(contextToken, file) ||
		isInJsxText(contextToken, location) ||
		ast.IsBigIntLiteral(contextToken)
}

func isInStringOrRegularExpressionOrTemplateLiteral(contextToken *ast.Node, position int) bool {
	// To be "in" one of these literals, the position has to be:
	//   1. entirely within the token text.
	//   2. at the end position of an unterminated token.
	//   3. at the end of a regular expression (due to trailing flags like '/foo/g').
	return (ast.IsRegularExpressionLiteral(contextToken) || ast.IsStringTextContainingNode(contextToken)) &&
		contextToken.Loc.ContainsExclusive(position) ||
		position == contextToken.End() &&
			(ast.IsUnterminatedLiteral(contextToken) || ast.IsRegularExpressionLiteral(contextToken))
}

// true if we are certain that the currently edited location must define a new location; false otherwise.
func isSolelyIdentifierDefinitionLocation(
	contextToken *ast.Node,
	previousToken *ast.Node,
	file *ast.SourceFile,
	position int,
	typeChecker *checker.Checker,
) bool {
	parent := contextToken.Parent
	containingNodeKind := parent.Kind
	switch contextToken.Kind {
	case ast.KindCommaToken:
		return containingNodeKind == ast.KindVariableDeclaration ||
			isVariableDeclarationListButNotTypeArgument(contextToken, file, typeChecker) ||
			containingNodeKind == ast.KindVariableStatement ||
			containingNodeKind == ast.KindEnumDeclaration || // enum a { foo, |
			isFunctionLikeButNotConstructor(containingNodeKind) ||
			containingNodeKind == ast.KindInterfaceDeclaration || // interface A<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)
	}

	tokenKind := keywordForNode(contextToken)
	// If the previous token is keyword corresponding to class member completion keyword
	// there will be completion available here
	if isClassMemberCompletionKeyword(tokenKind) && isFromObjectTypeDeclaration(contextToken) {
		return false
	}

	if isConstructorParameterCompletion(contextToken) {
		// constructor parameter completion is available only if
		// - its modifier of the constructor parameter or
		// - its name of the parameter and not being edited
		// eg. constructor(a |<- this shouldnt show completion
		if !ast.IsIdentifier(contextToken) ||
			ast.IsParameterPropertyModifier(tokenKind) ||
			isCurrentlyEditingNode(contextToken, file, position) {
			return false
		}
	}

	// Previous token may have been a keyword that was converted to an identifier.
	switch keywordForNode(contextToken) {
	case ast.KindAbstractKeyword, ast.KindClassKeyword, ast.KindDeclareKeyword,
		ast.KindEnumKeyword, ast.KindFunctionKeyword, ast.KindInterfaceKeyword, ast.KindLetKeyword,
		ast.KindPrivateKeyword, ast.KindProtectedKeyword, ast.KindPublicKeyword,
		ast.KindStaticKeyword, ast.KindVarKeyword:
		return true
	case ast.KindAsyncKeyword:
		return ast.IsPropertyDeclaration(contextToken.Parent)
	}

	// If we are inside a class declaration, and `constructor` is totally not present,
	// but we request a completion manually at a whitespace...
	ancestorClassLike := ast.FindAncestor(parent, ast.IsClassLike)
	if ancestorClassLike != nil && contextToken == previousToken &&
		isPreviousPropertyDeclarationTerminated(contextToken, file, position) {
		// Don't block completions.
		return false
	}

	ancestorPropertyDeclaration := ast.FindAncestor(parent, ast.IsPropertyDeclaration)
	// If we are inside a class declaration and typing `constructor` after property declaration...
	if ancestorPropertyDeclaration != nil && contextToken != previousToken &&
		ast.IsClassLike(previousToken.Parent.Parent) &&
		// And the cursor is at the token...
		position <= previousToken.End() {
		// If we are sure that the previous property declaration is terminated according to newline or semicolon...
		if isPreviousPropertyDeclarationTerminated(contextToken, file, previousToken.End()) {
			// Don't block completions.
			return false
		} else if contextToken.Kind != ast.KindEqualsToken &&
			// Should not block: `class C { blah = c/**/ }`
			// But should block: `class C { blah = somewhat c/**/ }` and `class C { blah: SomeType c/**/ }`
			(ast.IsInitializedProperty(ancestorPropertyDeclaration) || ancestorPropertyDeclaration.Type() != nil) {
			return true
		}
	}
	if tokenKind == ast.KindConstKeyword {
		return true
	}
	return ast.IsDeclarationName(contextToken) &&
		!ast.IsShorthandPropertyAssignment(parent) &&
		!ast.IsJsxAttribute(parent) &&
		// Don't block completions if we're in `class C /**/`, `interface I /**/` or `<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 clientSupportsItemLabelDetails(ctx context.Context) bool {
	return lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionItem.LabelDetailsSupport
}

func clientSupportsItemSnippet(ctx context.Context) bool {
	return lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionItem.SnippetSupport
}

func clientSupportsItemCommitCharacters(ctx context.Context) bool {
	return lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionItem.CommitCharactersSupport
}

func clientSupportsItemInsertReplace(ctx context.Context) bool {
	return lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionItem.InsertReplaceSupport
}

func clientSupportsDefaultCommitCharacters(ctx context.Context) bool {
	return slices.Contains(lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionList.ItemDefaults, "commitCharacters")
}

func clientSupportsDefaultEditRange(ctx context.Context) bool {
	return slices.Contains(lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionList.ItemDefaults, "editRange")
}

type argumentInfoForCompletions struct {
	invocation    *ast.CallLikeExpression
	argumentIndex int
	argumentCount int
}

func getArgumentInfoForCompletions(node *ast.Node, position int, file *ast.SourceFile, typeChecker *checker.Checker) *argumentInfoForCompletions {
	info := getImmediatelyContainingArgumentInfo(node, position, file, typeChecker)
	if info == nil || info.isTypeParameterList || info.invocation.callInvocation == nil {
		return nil
	}
	return &argumentInfoForCompletions{
		invocation:    info.invocation.callInvocation.node,
		argumentIndex: info.argumentIndex,
		argumentCount: info.argumentCount,
	}
}

// Special values for `CompletionInfo['source']` used to disambiguate
// completion items with the same `name`. (Each completion item must
// have a unique name/source combination, because those two fields
// comprise `CompletionEntryIdentifier` in `getCompletionEntryDetails`.
//
// When the completion item is an auto-import suggestion, the source
// is the module specifier of the suggestion. To avoid collisions,
// the values here should not be a module specifier we would ever
// generate for an auto-import.
const (
	// Completions that require `this.` insertion text
	SourceThisProperty = "ThisProperty/"
	// Auto-import that comes attached to a class member snippet
	SourceClassMemberSnippet = "ClassMemberSnippet/"
	// A type-only import that needs to be promoted in order to be used at the completion location
	SourceTypeOnlyAlias = "TypeOnlyAlias/"
	// Auto-import that comes attached to an object literal method snippet
	SourceObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/"
	// Case completions for switch statements
	SourceSwitchCases = "SwitchCases/"
	// Completions for an object literal expression
	SourceObjectLiteralMemberWithComma = "ObjectLiteralMemberWithComma/"
)

func (l *LanguageService) ResolveCompletionItem(
	ctx context.Context,
	item *lsproto.CompletionItem,
	data *lsproto.CompletionItemData,
) (*lsproto.CompletionItem, error) {
	if data == nil {
		return nil, errors.New("completion item data is nil")
	}

	program, file := l.tryGetProgramAndFile(data.FileName)
	if file == nil {
		return nil, fmt.Errorf("file not found: %s", data.FileName)
	}

	checker, done := program.GetTypeCheckerForFile(ctx, file)
	defer done()
	return l.getCompletionItemDetails(ctx, program, checker, int(data.Position), file, item, data), nil
}

func getCompletionDocumentationFormat(ctx context.Context) lsproto.MarkupKind {
	return lsproto.PreferredMarkupKind(lsproto.GetClientCapabilities(ctx).TextDocument.Completion.CompletionItem.DocumentationFormat)
}

func (l *LanguageService) getCompletionItemDetails(
	ctx context.Context,
	program *compiler.Program,
	checker *checker.Checker,
	position int,
	file *ast.SourceFile,
	item *lsproto.CompletionItem,
	data *lsproto.CompletionItemData,
) *lsproto.CompletionItem {
	docFormat := getCompletionDocumentationFormat(ctx)
	contextToken, previousToken := getRelevantTokens(position, file)
	if IsInString(file, position, previousToken) {
		return l.getStringLiteralCompletionDetails(
			ctx,
			checker,
			item,
			data.Name,
			file,
			position,
			contextToken,
			docFormat,
		)
	}

	if data.AutoImport != nil {
		edits, description := (&autoimport.Fix{AutoImportFix: data.AutoImport}).Edits(ctx, file, program.Options(), l.FormatOptions(), l.converters, l.UserPreferences())
		item.AdditionalTextEdits = &edits
		item.Detail = strPtrTo(description)
		return item
	}

	// Compute all the completion symbols again.
	symbolCompletion := l.getSymbolCompletionFromItemData(
		ctx,
		checker,
		file,
		position,
		data,
	)
	preferences := l.UserPreferences()

	switch {
	case symbolCompletion.request != nil:
		request := *symbolCompletion.request
		switch request := request.(type) {
		case *completionDataJSDocTagName:
			return createSimpleDetails(item, data.Name, docFormat)
		case *completionDataJSDocTag:
			return createSimpleDetails(item, data.Name, docFormat)
		case *completionDataJSDocParameterName:
			return createSimpleDetails(item, data.Name, docFormat)
		case *completionDataKeyword:
			if core.Some(request.keywordCompletions, func(c *CompletionItem) bool {
				return c.Label == data.Name
			}) {
				return createSimpleDetails(item, data.Name, docFormat)
			}
			return item
		default:
			panic(fmt.Sprintf("Unexpected completion data type: %T", request))
		}
	case symbolCompletion.symbol != nil:
		symbolDetails := symbolCompletion.symbol
		return l.createCompletionDetailsForSymbol(
			item,
			symbolDetails.symbol,
			checker,
			symbolDetails.location,
			position,
			docFormat,
		)
	case symbolCompletion.literal != nil:
		literal := symbolCompletion.literal
		return createSimpleDetails(item, completionNameForLiteral(file, preferences, *literal), docFormat)
	case symbolCompletion.cases != nil:
		return item
	default:
		// Didn't find a symbol with this name.  See if we can find a keyword instead.
		if core.Some(allKeywordCompletions(), func(c *lsproto.CompletionItem) bool {
			return c.Label == data.Name
		}) {
			return createSimpleDetails(item, data.Name, docFormat)
		}
		return item
	}
}

type detailsData struct {
	symbol  *symbolDetails
	request *completionData
	literal *literalValue
	cases   *struct{}
}

type symbolDetails struct {
	symbol             *ast.Symbol
	location           *ast.Node
	origin             *symbolOriginInfo
	previousToken      *ast.Node
	contextToken       *ast.Node
	jsxInitializer     jsxInitializer
	isTypeOnlyLocation bool
}

func (l *LanguageService) getSymbolCompletionFromItemData(
	ctx context.Context,
	ch *checker.Checker,
	file *ast.SourceFile,
	position int,
	itemData *lsproto.CompletionItemData,
) detailsData {
	if itemData.Source == SourceSwitchCases {
		return detailsData{
			cases: &struct{}{},
		}
	}

	completionData, err := l.getCompletionData(ctx, ch, file, position, l.UserPreferences(), true /*forItemResolve*/)
	if err != nil {
		panic(err)
	}

	if completionData == nil {
		return detailsData{}
	}

	if _, ok := completionData.(*completionDataData); !ok {
		return detailsData{
			request: &completionData,
		}
	}

	data := completionData.(*completionDataData)

	preferences := l.UserPreferences()
	var literal literalValue
	for _, l := range data.literals {
		if completionNameForLiteral(file, preferences, l) == itemData.Name {
			literal = l
			break
		}
	}
	if literal != nil {
		return detailsData{
			literal: &literal,
		}
	}

	// Find the symbol with the matching entry name.
	// We don't need to perform character checks here because we're only comparing the
	// name against 'entryName' (which is known to be good), not building a new
	// completion entry.
	for index, symbol := range data.symbols {
		origin := data.symbolToOriginInfoMap[index]
		displayName, _ := getCompletionEntryDisplayNameForSymbol(symbol, origin, data.completionKind, data.isJsxIdentifierExpected)
		if displayName == itemData.Name &&
			(itemData.Source == string(completionSourceClassMemberSnippet) && symbol.Flags&ast.SymbolFlagsClassMember != 0 ||
				itemData.Source == string(completionSourceObjectLiteralMethodSnippet) && symbol.Flags&(ast.SymbolFlagsProperty|ast.SymbolFlagsMethod) != 0 ||
				getSourceFromOrigin(origin) == itemData.Source ||
				itemData.Source == string(completionSourceObjectLiteralMemberWithComma)) {
			return detailsData{
				symbol: &symbolDetails{
					symbol:             symbol,
					location:           data.location,
					origin:             origin,
					previousToken:      data.previousToken,
					contextToken:       data.contextToken,
					jsxInitializer:     data.jsxInitializer,
					isTypeOnlyLocation: data.isTypeOnlyLocation,
				},
			}
		}
	}
	return detailsData{}
}

func createSimpleDetails(
	item *lsproto.CompletionItem,
	name string,
	docFormat lsproto.MarkupKind,
) *lsproto.CompletionItem {
	return createCompletionDetails(item, name, "" /*documentation*/, docFormat)
}

func createCompletionDetails(
	item *lsproto.CompletionItem,
	detail string,
	documentation string,
	docFormat lsproto.MarkupKind,
) *lsproto.CompletionItem {
	// !!! fill in additionalTextEdits from code actions
	if item.Detail == nil && detail != "" {
		item.Detail = &detail
	}
	if documentation != "" {
		item.Documentation = &lsproto.StringOrMarkupContent{
			MarkupContent: &lsproto.MarkupContent{
				Kind:  docFormat,
				Value: documentation,
			},
		}
	}
	return item
}

type codeAction struct {
	// Description of the code action to display in the UI of the editor
	description string
	// Text changes to apply to each file as part of the code action
	changes []*lsproto.TextEdit
}

func (l *LanguageService) createCompletionDetailsForSymbol(
	item *lsproto.CompletionItem,
	symbol *ast.Symbol,
	checker *checker.Checker,
	location *ast.Node,
	position int,
	docFormat lsproto.MarkupKind,
) *lsproto.CompletionItem {
	quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(checker, symbol, location, docFormat, nil)
	return createCompletionDetails(item, quickInfo, documentation, docFormat)
}

func (l *LanguageService) getImportStatementCompletionInfo(contextToken *ast.Node, sourceFile *ast.SourceFile) importStatementCompletionInfo {
	result := importStatementCompletionInfo{}
	var candidate *ast.Node
	parent := contextToken.Parent
	switch {
	case ast.IsImportEqualsDeclaration(parent):
		// import Foo |
		// import Foo f|
		lastToken := lsutil.GetLastToken(parent, sourceFile)
		if contextToken.Kind == ast.KindIdentifier && lastToken != contextToken {
			result.keywordCompletion = ast.KindFromKeyword
			result.isKeywordOnlyCompletion = true
		} else {
			if contextToken.Kind != ast.KindTypeKeyword {
				result.keywordCompletion = ast.KindTypeKeyword
			}
			if isModuleSpecifierMissingOrEmpty(parent.AsImportEqualsDeclaration().ModuleReference) {
				candidate = parent
			}
		}

	case couldBeTypeOnlyImportSpecifier(parent, contextToken) && canCompleteFromNamedBindings(parent.Parent):
		candidate = parent
	case ast.IsNamedImports(parent) || ast.IsNamespaceImport(parent):
		if !parent.Parent.IsTypeOnly() && (contextToken.Kind == ast.KindOpenBraceToken ||
			contextToken.Kind == ast.KindImportKeyword ||
			contextToken.Kind == ast.KindCommaToken) {
			result.keywordCompletion = ast.KindTypeKeyword
		}
		if canCompleteFromNamedBindings(parent) {
			// At `import { ... } |` or `import * as Foo |`, the only possible completion is `from`
			if contextToken.Kind == ast.KindCloseBraceToken || contextToken.Kind == ast.KindIdentifier {
				result.isKeywordOnlyCompletion = true
				result.keywordCompletion = ast.KindFromKeyword
			} else {
				candidate = parent.Parent.Parent
			}
		}

	case ast.IsExportDeclaration(parent) && contextToken.Kind == ast.KindAsteriskToken,
		ast.IsNamedExports(parent) && contextToken.Kind == ast.KindCloseBraceToken:
		result.isKeywordOnlyCompletion = true
		result.keywordCompletion = ast.KindFromKeyword

	case contextToken.Kind == ast.KindImportKeyword:
		if ast.IsSourceFile(parent) {
			// A lone import keyword with nothing following it does not parse as a statement at all
			result.keywordCompletion = ast.KindTypeKeyword
			candidate = contextToken
		} else if ast.IsImportDeclaration(parent) {
			// `import s| from`
			result.keywordCompletion = ast.KindTypeKeyword
			if isModuleSpecifierMissingOrEmpty(parent.ModuleSpecifier()) {
				candidate = parent
			}
		}
	}

	if candidate != nil {
		result.isNewIdentifierLocation = true
		result.replacementSpan = l.getSingleLineReplacementSpanForImportCompletionNode(candidate)
		result.couldBeTypeOnlyImportSpecifier = couldBeTypeOnlyImportSpecifier(candidate, contextToken)
		if ast.IsImportDeclaration(candidate) {
			if importClause := candidate.ImportClause(); importClause != nil {
				result.isTopLevelTypeOnly = importClause.IsTypeOnly()
			}
		} else if candidate.Kind == ast.KindImportEqualsDeclaration {
			result.isTopLevelTypeOnly = candidate.IsTypeOnly()
		}
	} else {
		result.isNewIdentifierLocation = result.keywordCompletion == ast.KindTypeKeyword
	}
	return result
}

func (l *LanguageService) getSingleLineReplacementSpanForImportCompletionNode(node *ast.Node) *lsproto.Range {
	// node is ImportDeclaration | ImportEqualsDeclaration | ImportSpecifier | JSDocImportTag | Token<SyntaxKind.ImportKeyword>
	if ancestor := ast.FindAncestor(node, core.Or(ast.IsImportDeclaration, ast.IsImportEqualsDeclaration, ast.IsJSDocImportTag)); ancestor != nil {
		node = ancestor
	}
	sourceFile := ast.GetSourceFileOfNode(node)
	// Use token position (excluding JSDoc/trivia) instead of node.Pos() to avoid including JSDoc comments
	tokenPos := scanner.GetTokenPosOfNode(node, sourceFile, false /*includeJSDoc*/)
	if printer.GetLinesBetweenPositions(sourceFile, tokenPos, node.End()) == 0 {
		return new(l.createLspRangeFromNode(node, sourceFile))
	}

	if node.Kind == ast.KindImportKeyword || node.Kind == ast.KindImportSpecifier {
		panic("ImportKeyword was necessarily on one line; ImportSpecifier was necessarily parented in an ImportDeclaration")
	}

	// Guess which point in the import might actually be a later statement parsed as part of the import
	// during parser recovery - either in the middle of named imports, or the module specifier.
	var potentialSplitPoint *ast.Node
	if node.Kind == ast.KindImportDeclaration || node.Kind == ast.KindJSDocImportTag {
		var specifier *ast.Node
		if importClause := node.ImportClause(); importClause != nil {
			specifier = getPotentiallyInvalidImportSpecifier(importClause.AsImportClause().NamedBindings)
		}

		if specifier != nil {
			potentialSplitPoint = specifier
		} else {
			potentialSplitPoint = node.ModuleSpecifier()
		}
	} else {
		potentialSplitPoint = node.AsImportEqualsDeclaration().ModuleReference
	}

	withoutModuleSpecifier := core.NewTextRange(scanner.GetTokenPosOfNode(lsutil.GetFirstToken(node, sourceFile), sourceFile, false), potentialSplitPoint.Pos())
	// The module specifier/reference was previously found to be missing, empty, or
	// not a string literal - in this last case, it's likely that statement on a following
	// line was parsed as the module specifier of a partially-typed import, e.g.
	//   import Foo|
	//   interface Blah {}
	// This appears to be a multiline-import, and editors can't replace multiple lines.
	// But if everything but the "module specifier" is on one line, by this point we can
	// assume that the "module specifier" is actually just another statement, and return
	// the single-line range of the import excluding that probable statement.
	if printer.GetLinesBetweenPositions(sourceFile, withoutModuleSpecifier.Pos(), withoutModuleSpecifier.End()) == 0 {
		return new(l.createLspRangeFromBounds(withoutModuleSpecifier.Pos(), withoutModuleSpecifier.End(), sourceFile))
	}
	return nil
}

func couldBeTypeOnlyImportSpecifier(importSpecifier *ast.Node, contextToken *ast.Node) bool {
	return ast.IsImportSpecifier(importSpecifier) && (importSpecifier.IsTypeOnly() || contextToken == importSpecifier.Name() && isTypeKeywordTokenOrIdentifier(contextToken))
}

func canCompleteFromNamedBindings(namedBindings *ast.NamedImportBindings) bool {
	if !isModuleSpecifierMissingOrEmpty(namedBindings.Parent.Parent.ModuleSpecifier()) || namedBindings.Parent.Name() != nil {
		return false
	}
	if ast.IsNamedImports(namedBindings) {
		// We can only complete on named imports if there are no other named imports already,
		// but parser recovery sometimes puts later statements in the named imports list, so
		// we try to only consider the probably-valid ones.
		invalidNamedImport := getPotentiallyInvalidImportSpecifier(namedBindings)
		elements := namedBindings.Elements()
		validImports := len(elements)
		if invalidNamedImport != nil {
			validImports = slices.Index(elements, invalidNamedImport)
		}

		return validImports < 2 && validImports > -1
	}
	return true
}

// Tries to identify the first named import that is not really a named import, but rather
// just parser recovery for a situation like:
//
//	import { Foo|
//	interface Bar {}
//
// in which `Foo`, `interface`, and `Bar` are all parsed as import specifiers. The caller
// will also check if this token is on a separate line from the rest of the import.
func getPotentiallyInvalidImportSpecifier(namedBindings *ast.NamedImportBindings) *ast.Node {
	if namedBindings == nil || namedBindings.Kind != ast.KindNamedImports {
		return nil
	}
	return core.Find(namedBindings.Elements(), func(e *ast.Node) bool {
		return e.PropertyName() == nil && lsutil.IsNonContextualKeyword(scanner.StringToToken(e.Name().Text())) &&
			astnav.FindPrecedingToken(ast.GetSourceFileOfNode(namedBindings), e.Name().Pos()).Kind != ast.KindCommaToken
	})
}

func isModuleSpecifierMissingOrEmpty(specifier *ast.Expression) bool {
	if ast.NodeIsMissing(specifier) {
		return true
	}
	node := specifier
	if ast.IsExternalModuleReference(node) {
		node = node.Expression()
	}
	if !ast.IsStringLiteralLike(node) {
		return true
	}
	return node.Text() == ""
}

func hasDocComment(file *ast.SourceFile, position int) bool {
	token := astnav.GetTokenAtPosition(file, position)
	return ast.FindAncestor(token, (*ast.Node).IsJSDoc) != nil
}

// Get the corresponding JSDocTag node if the position is in a JSDoc comment
func getJSDocTagAtPosition(node *ast.Node, position int) *ast.Node {
	return ast.FindAncestorOrQuit(node, func(n *ast.Node) ast.FindAncestorResult {
		if ast.IsJSDocTag(n) && n.Loc.ContainsInclusive(position) {
			return ast.FindAncestorTrue
		}
		if n.IsJSDoc() {
			return ast.FindAncestorQuit
		}
		return ast.FindAncestorFalse
	})
}

func tryGetTypeExpressionFromTag(tag *ast.Node) *ast.Node {
	if isTagWithTypeExpression(tag) {
		var typeExpression *ast.Node
		if ast.IsJSDocTemplateTag(tag) {
			typeExpression = tag.AsJSDocTemplateTag().Constraint
		} else {
			typeExpression = tag.TypeExpression()
		}
		if typeExpression != nil && typeExpression.Kind == ast.KindJSDocTypeExpression {
			return typeExpression
		}
	}
	if ast.IsJSDocAugmentsTag(tag) || ast.IsJSDocImplementsTag(tag) {
		return tag.ClassName()
	}
	return nil
}

func isTagWithTypeExpression(tag *ast.Node) bool {
	switch tag.Kind {
	case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag, ast.KindJSDocReturnTag, ast.KindJSDocTypeTag,
		ast.KindJSDocTypedefTag, ast.KindJSDocThrowsTag, ast.KindJSDocSatisfiesTag:
		return true
	case ast.KindJSDocTemplateTag:
		return tag.AsJSDocTemplateTag().Constraint != nil
	default:
		return false
	}
}

func (l *LanguageService) jsDocCompletionInfo(
	ctx context.Context,
	position int,
	file *ast.SourceFile,
	items []*CompletionItem,
) *CompletionList {
	defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/)
	itemDefaults := l.setItemDefaults(
		ctx,
		position,
		file,
		items,
		&defaultCommitCharacters,
		nil, /*optionalReplacementSpan*/
	)
	return &CompletionList{
		IsIncomplete: false,
		ItemDefaults: itemDefaults,
		Items:        items,
	}
}

var jsDocTagNames = []string{
	"abstract",
	"access",
	"alias",
	"argument",
	"async",
	"augments",
	"author",
	"borrows",
	"callback",
	"class",
	"classdesc",
	"constant",
	"constructor",
	"constructs",
	"copyright",
	"default",
	"deprecated",
	"description",
	"emits",
	"enum",
	"event",
	"example",
	"exports",
	"extends",
	"external",
	"field",
	"file",
	"fileoverview",
	"fires",
	"function",
	"generator",
	"global",
	"hideconstructor",
	"host",
	"ignore",
	"implements",
	"import",
	"inheritdoc",
	"inner",
	"instance",
	"interface",
	"kind",
	"lends",
	"license",
	"link",
	"linkcode",
	"linkplain",
	"listens",
	"member",
	"memberof",
	"method",
	"mixes",
	"module",
	"name",
	"namespace",
	"overload",
	"override",
	"package",
	"param",
	"private",
	"prop",
	"property",
	"protected",
	"public",
	"readonly",
	"requires",
	"returns",
	"satisfies",
	"see",
	"since",
	"static",
	"summary",
	"template",
	"this",
	"throws",
	"todo",
	"tutorial",
	"type",
	"typedef",
	"var",
	"variation",
	"version",
	"virtual",
	"yields",
}

var jsDocTagNameCompletionItems = sync.OnceValue(func() []*lsproto.CompletionItem {
	items := make([]*lsproto.CompletionItem, 0, len(jsDocTagNames))
	for _, tagName := range jsDocTagNames {
		item := &lsproto.CompletionItem{
			Label:    tagName,
			Kind:     new(lsproto.CompletionItemKindKeyword),
			SortText: new(string(SortTextLocationPriority)),
		}
		items = append(items, item)
	}
	return items
})

var jsDocTagCompletionItems = sync.OnceValue(func() []*lsproto.CompletionItem {
	items := make([]*lsproto.CompletionItem, 0, len(jsDocTagNames))
	for _, tagName := range jsDocTagNames {
		item := &lsproto.CompletionItem{
			Label:    "@" + tagName,
			Kind:     new(lsproto.CompletionItemKindKeyword),
			SortText: new(string(SortTextLocationPriority)),
		}
		items = append(items, item)
	}
	return items
})

func getJSDocTagNameCompletions() []*CompletionItem {
	return cloneItems(jsDocTagNameCompletionItems())
}

func getJSDocTagCompletions() []*CompletionItem {
	return cloneItems(jsDocTagCompletionItems())
}

func getJSDocParameterCompletions(
	ctx context.Context,
	file *ast.SourceFile,
	position int,
	typeChecker *checker.Checker,
	options *core.CompilerOptions,
	preferences lsutil.UserPreferences,
	tagNameOnly bool,
) []*CompletionItem {
	currentToken := astnav.GetTokenAtPosition(file, position)
	if !ast.IsJSDocTag(currentToken) && !currentToken.IsJSDoc() {
		return nil
	}
	var jsDoc *ast.JSDocNode
	if currentToken.IsJSDoc() {
		jsDoc = currentToken
	} else {
		jsDoc = currentToken.Parent
	}
	if !jsDoc.IsJSDoc() {
		return nil
	}
	fun := jsDoc.Parent
	if !ast.IsFunctionLike(fun) {
		return nil
	}

	isJS := ast.IsSourceFileJS(file)
	// isSnippet := clientSupportsItemSnippet(clientOptions)
	isSnippet := false // !!! need snippet printer
	paramTagCount := 0
	var tags []*ast.Node
	if jsDoc.AsJSDoc().Tags != nil {
		tags = jsDoc.AsJSDoc().Tags.Nodes
	}
	for _, tag := range tags {
		if ast.IsJSDocParameterTag(tag) &&
			astnav.GetStartOfNode(tag, file, false /*includeJSDoc*/) < position &&
			ast.IsIdentifier(tag.Name()) {
			paramTagCount++
		}
	}
	paramIndex := -1
	return core.MapNonNil(fun.Parameters(), func(param *ast.ParameterDeclarationNode) *CompletionItem {
		paramIndex++
		if paramIndex < paramTagCount {
			// This parameter is already annotated.
			return nil
		}
		if ast.IsIdentifier(param.Name()) { // Named parameter
			tabstopCounter := 1
			paramName := param.Name().Text()
			displayText := getJSDocParamAnnotation(
				paramName,
				param.Initializer(),
				param.AsParameterDeclaration().DotDotDotToken,
				isJS,
				/*isObject*/ false,
				/*isSnippet*/ false,
				typeChecker,
				options,
				preferences,
				&tabstopCounter,
			)
			var snippetText string
			if isSnippet {
				snippetText = getJSDocParamAnnotation(
					paramName,
					param.Initializer(),
					param.AsParameterDeclaration().DotDotDotToken,
					isJS,
					/*isObject*/ false,
					/*isSnippet*/ true,
					typeChecker,
					options,
					preferences,
					&tabstopCounter,
				)
			}
			if tagNameOnly { // Remove `@`
				displayText = displayText[1:]
				if snippetText != "" {
					snippetText = snippetText[1:]
				}
			}

			return &CompletionItem{
				CompletionItem: &lsproto.CompletionItem{
					Label:            displayText,
					Kind:             new(lsproto.CompletionItemKindVariable),
					SortText:         new(string(SortTextLocationPriority)),
					InsertText:       strPtrTo(snippetText),
					InsertTextFormat: core.IfElse(isSnippet, new(lsproto.InsertTextFormatSnippet), nil),
				},
			}
		} else if paramIndex == paramTagCount {
			// Destructuring parameter; do it positionally
			paramPath := fmt.Sprintf("param%d", paramIndex)
			displayTextResult := generateJSDocParamTagsForDestructuring(
				paramPath,
				param.Name(),
				param.Initializer(),
				param.AsParameterDeclaration().DotDotDotToken,
				isJS,
				/*isSnippet*/ false,
				typeChecker,
				options,
				preferences,
			)
			var snippetText string
			if isSnippet {
				snippetTextResult := generateJSDocParamTagsForDestructuring(
					paramPath,
					param.Name(),
					param.Initializer(),
					param.AsParameterDeclaration().DotDotDotToken,
					isJS,
					/*isSnippet*/ true,
					typeChecker,
					options,
					preferences,
				)
				snippetText = strings.Join(snippetTextResult, options.NewLine.GetNewLineCharacter()+"* ")
			}
			displayText := strings.Join(displayTextResult, options.NewLine.GetNewLineCharacter()+"* ")
			if tagNameOnly { // Remove `@`
				displayText = strings.TrimPrefix(displayText, "@")
				snippetText = strings.TrimPrefix(snippetText, "@")
			}
			return &CompletionItem{
				CompletionItem: &lsproto.CompletionItem{
					Label:            displayText,
					Kind:             new(lsproto.CompletionItemKindVariable),
					SortText:         new(string(SortTextLocationPriority)),
					InsertText:       strPtrTo(snippetText),
					InsertTextFormat: core.IfElse(isSnippet, new(lsproto.InsertTextFormatSnippet), nil),
				},
			}
		}
		return nil
	})
}

func getJSDocParamAnnotation(
	paramName string,
	initializer *ast.Expression,
	dotDotDotToken *ast.TokenNode,
	isJS bool,
	isObject bool,
	isSnippet bool,
	typeChecker *checker.Checker,
	options *core.CompilerOptions,
	preferences lsutil.UserPreferences,
	tabstopCounter *int,
) string {
	if isSnippet {
		debug.Assert(tabstopCounter != nil)
	}
	if initializer != nil {
		paramName = getJSDocParamNameWithInitializer(paramName, initializer)
	}
	if isSnippet {
		paramName = escapeSnippetText(paramName)
	}
	if isJS {
		t := "*"
		if isObject {
			debug.Assert(dotDotDotToken == nil, `Cannot annotate a rest parameter with type 'object'.`)
			t = "object"
		} else {
			if initializer != nil {
				inferredType := typeChecker.GetTypeAtLocation(initializer.Parent)
				if inferredType.Flags()&(checker.TypeFlagsAny|checker.TypeFlagsVoid) == 0 {
					file := ast.GetSourceFileOfNode(initializer)
					quotePreference := lsutil.GetQuotePreference(file, preferences)
					builderFlags := core.IfElse(
						quotePreference == lsutil.QuotePreferenceSingle,
						nodebuilder.FlagsUseSingleQuotesForStringLiteralType,
						nodebuilder.FlagsNone,
					)
					typeNode := typeChecker.TypeToTypeNode(
						inferredType,
						ast.FindAncestor(initializer, ast.IsFunctionLike),
						builderFlags,
						nil, /*idToSymbol*/
					)
					if typeNode != nil {
						emitContext := printer.NewEmitContext()
						// !!! snippet p
						p := printer.NewPrinter(printer.PrinterOptions{
							RemoveComments: true,
							// !!!
							// Module: options.Module,
							// ModuleResolution: options.ModuleResolution,
							// Target: options.Target,
						}, printer.PrintHandlers{}, emitContext)
						emitContext.SetEmitFlags(typeNode, printer.EFSingleLine)
						t = p.Emit(typeNode, file)
					}
				}
			}
			if isSnippet && t == "*" {
				tabstop := *tabstopCounter
				*tabstopCounter++
				t = fmt.Sprintf("${%d:%s}", tabstop, t)
			}
		}
		dotDotDot := core.IfElse(!isObject && dotDotDotToken != nil, "...", "")
		var description string
		if isSnippet {
			tabstop := *tabstopCounter
			*tabstopCounter++
			description = fmt.Sprintf("${%d}", tabstop)
		}
		return fmt.Sprintf("@param {%s%s} %s %s", dotDotDot, t, paramName, description)
	} else {
		var description string
		if isSnippet {
			tabstop := *tabstopCounter
			*tabstopCounter++
			description = fmt.Sprintf("${%d}", tabstop)
		}
		return fmt.Sprintf("@param %s %s", paramName, description)
	}
}

func getJSDocParamNameWithInitializer(paramName string, initializer *ast.Expression) string {
	initializerText := strings.TrimSpace(scanner.GetTextOfNode(initializer))
	if strings.Contains(initializerText, "\n") || len(initializerText) > 80 {
		return fmt.Sprintf("[%s]", paramName)
	}
	return fmt.Sprintf("[%s=%s]", paramName, initializerText)
}

func generateJSDocParamTagsForDestructuring(
	path string,
	pattern *ast.BindingPatternNode,
	initializer *ast.Expression,
	dotDotDotToken *ast.TokenNode,
	isJS bool,
	isSnippet bool,
	typeChecker *checker.Checker,
	options *core.CompilerOptions,
	preferences lsutil.UserPreferences,
) []string {
	tabstopCounter := 1
	if !isJS {
		return []string{getJSDocParamAnnotation(
			path,
			initializer,
			dotDotDotToken,
			isJS,
			/*isObject*/ false,
			isSnippet,
			typeChecker,
			options,
			preferences,
			&tabstopCounter,
		)}
	}
	return jsDocParamPatternWorker(
		path,
		pattern,
		initializer,
		dotDotDotToken,
		isJS,
		isSnippet,
		typeChecker,
		options,
		preferences,
		&tabstopCounter,
	)
}

func jsDocParamPatternWorker(
	path string,
	pattern *ast.BindingPatternNode,
	initializer *ast.Expression,
	dotDotDotToken *ast.TokenNode,
	isJS bool,
	isSnippet bool,
	typeChecker *checker.Checker,
	options *core.CompilerOptions,
	preferences lsutil.UserPreferences,
	counter *int,
) []string {
	if ast.IsObjectBindingPattern(pattern) && dotDotDotToken == nil {
		childCounter := *counter
		rootParam := getJSDocParamAnnotation(
			path,
			initializer,
			dotDotDotToken,
			isJS,
			/*isObject*/ true,
			isSnippet,
			typeChecker,
			options,
			preferences,
			&childCounter,
		)
		var childTags []string
		for _, element := range pattern.Elements() {
			elementTags := jsDocParamElementWorker(
				path,
				element,
				initializer,
				dotDotDotToken,
				isJS,
				isSnippet,
				typeChecker,
				options,
				preferences,
				&childCounter,
			)
			if len(elementTags) == 0 {
				childTags = nil
				break
			}
			childTags = append(childTags, elementTags...)
		}
		if len(childTags) > 0 {
			*counter = childCounter
			return append([]string{rootParam}, childTags...)
		}
	}
	return []string{
		getJSDocParamAnnotation(
			path,
			initializer,
			dotDotDotToken,
			isJS,
			/*isObject*/ false,
			isSnippet,
			typeChecker,
			options,
			preferences,
			counter,
		),
	}
}

// Assumes binding element is inside object binding pattern.
// We can't deeply annotate an array binding pattern.
func jsDocParamElementWorker(
	path string,
	element *ast.BindingElementNode,
	initializer *ast.Expression,
	dotDotDotToken *ast.TokenNode,
	isJS bool,
	isSnippet bool,
	typeChecker *checker.Checker,
	options *core.CompilerOptions,
	preferences lsutil.UserPreferences,
	counter *int,
) []string {
	if ast.IsIdentifier(element.Name()) { // `{ b }` or `{ b: newB }`
		var propertyName string
		if element.PropertyName() != nil {
			propertyName, _ = ast.TryGetTextOfPropertyName(element.PropertyName())
		} else {
			propertyName = element.Name().Text()
		}
		if propertyName == "" {
			return nil
		}
		paramName := fmt.Sprintf("%s.%s", path, propertyName)
		return []string{
			getJSDocParamAnnotation(
				paramName,
				element.Initializer(),
				element.AsBindingElement().DotDotDotToken,
				isJS,
				/*isObject*/ false,
				isSnippet,
				typeChecker,
				options,
				preferences,
				counter,
			),
		}
	} else if element.PropertyName() != nil { // `{ b: {...} }` or `{ b: [...] }`
		propertyName, _ := ast.TryGetTextOfPropertyName(element.PropertyName())
		if propertyName == "" {
			return nil
		}
		return jsDocParamPatternWorker(
			fmt.Sprintf("%s.%s", path, propertyName),
			element.Name(),
			element.Initializer(),
			element.AsBindingElement().DotDotDotToken,
			isJS,
			isSnippet,
			typeChecker,
			options,
			preferences,
			counter,
		)
	}
	return nil
}

func getJSDocParameterNameCompletions(tag *ast.JSDocParameterOrPropertyTag) []*CompletionItem {
	if !ast.IsIdentifier(tag.Name()) {
		return nil
	}
	nameThusFar := tag.Name().Text()
	jsDoc := tag.Parent
	fn := jsDoc.Parent
	if !ast.IsFunctionLike(fn) {
		return nil
	}

	var tags []*ast.Node
	if jsDoc.AsJSDoc().Tags != nil {
		tags = jsDoc.AsJSDoc().Tags.Nodes
	}

	return core.MapNonNil(fn.Parameters(), func(param *ast.ParameterDeclarationNode) *CompletionItem {
		if !ast.IsIdentifier(param.Name()) {
			return nil
		}

		name := param.Name().Text()
		if core.Some(tags, func(t *ast.Node) bool {
			return t != tag.AsNode() &&
				ast.IsJSDocParameterTag(t) &&
				ast.IsIdentifier(t.Name()) &&
				t.Name().Text() == name
		}) || nameThusFar != "" && !strings.HasPrefix(name, nameThusFar) {
			return nil
		}

		return &CompletionItem{
			CompletionItem: &lsproto.CompletionItem{
				Label:    name,
				Kind:     new(lsproto.CompletionItemKindVariable),
				SortText: new(string(SortTextLocationPriority)),
			},
		}
	})
}

func (l *LanguageService) getExhaustiveCaseSnippets(
	ctx context.Context,
	caseBlock *ast.CaseBlock,
	file *ast.SourceFile,
	position int,
	options *core.CompilerOptions,
	program *compiler.Program,
	c *checker.Checker,
) (*lsproto.CompletionItem, error) {
	clauses := caseBlock.Clauses.Nodes
	switchType := c.GetTypeAtLocation(caseBlock.AsNode().Parent.Expression())
	if switchType != nil && switchType.IsUnion() && core.Every(switchType.Types(), isLiteral) {
		// Collect constant values in existing clauses.
		tracker := newCaseClauseTracker(c, clauses)
		target := options.GetEmitScriptTarget()
		quotePreference := lsutil.GetQuotePreference(file, l.UserPreferences())
		// Tolerate a nil import adder in untitled files.
		var importAdder autoimport.ImportAdder
		if !tspath.IsDynamicFileName(file.FileName()) {
			view, err := l.getPreparedAutoImportView(file)
			if err != nil {
				return nil, err
			}
			if view != nil {
				importAdder = autoimport.NewImportAdder(
					ctx,
					program,
					c,
					file,
					view,
					l.FormatOptions(),
					l.converters,
					l.UserPreferences(),
				)
			}
		}

		var elements []*ast.Expression
		factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
		for _, t := range switchType.Types() {
			// Enums
			if t.IsEnumLiteral() {
				debug.Assert(t.Symbol() != nil, "An enum member type should have a symbol")
				debug.Assert(t.Symbol().Parent != nil, "An enum member type should have a parent symbol (the enum symbol)")
				// Filter existing enums by their values
				var enumValue any
				if t.Symbol().ValueDeclaration != nil {
					enumValue = c.GetConstantValue(t.Symbol().ValueDeclaration)
				}
				if enumValue != nil {
					if tracker.hasValue(enumValue) {
						continue
					}
					tracker.addValue(enumValue)
				}
				typeNode := autoimport.TypeToAutoImportableTypeNode(c, importAdder, t, caseBlock.AsNode())
				if typeNode == nil {
					return nil, nil
				}
				expr := typeNodeToExpression(typeNode, target, quotePreference, factory)
				if expr == nil {
					return nil, nil
				}
				elements = append(elements, expr)
			} else if value := t.AsLiteralType().Value(); !tracker.hasValue(value) { // Literals
				switch v := value.(type) {
				case jsnum.PseudoBigInt:
					var bigInt *ast.Node
					if v.Negative {
						v.Negative = false
						bigInt = factory.NewPrefixUnaryExpression(ast.KindMinusToken, factory.NewBigIntLiteral(v.String()+"n", ast.TokenFlagsNone))
					} else {
						bigInt = factory.NewBigIntLiteral(v.String()+"n", ast.TokenFlagsNone)
					}
					elements = append(elements, bigInt)
				case jsnum.Number:
					var number *ast.Node
					if v < 0 {
						number = factory.NewPrefixUnaryExpression(ast.KindMinusToken, factory.NewNumericLiteral(v.Abs().String(), ast.TokenFlagsNone))
					} else {
						number = factory.NewNumericLiteral(v.String(), ast.TokenFlagsNone)
					}
					elements = append(elements, number)
				case string:
					literal := factory.NewStringLiteral(v, core.IfElse(quotePreference == lsutil.QuotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone))
					elements = append(elements, literal)
				}
			}
		}
		if len(elements) == 0 {
			return nil, nil
		}

		newClauses := core.Map(elements, func(element *ast.Node) *ast.CaseOrDefaultClauseNode {
			return factory.NewCaseOrDefaultClause(ast.KindCaseClause, element, factory.NewNodeList(nil))
		})
		newLineChar := l.FormatOptions().NewLineCharacter
		printer := createSnippetPrinter(printer.PrinterOptions{
			RemoveComments: true,
			NewLine:        core.GetNewLineKind(newLineChar),
		})
		printNode := func(node *ast.Node) string { return printer.printAndFormatNode(ctx, node, file) }
		insertText := strings.Join(core.MapIndex(newClauses, func(clause *ast.Node, i int) string {
			if clientSupportsItemSnippet(ctx) {
				return fmt.Sprintf("%s$%d", printNode(clause), i+1)
			}
			return printer.printUnescapedNode(clause)
		}), newLineChar)

		firstClause := printer.printUnescapedNode(newClauses[0])
		name := firstClause + " ..."

		var additionalTextEdits *[]*lsproto.TextEdit
		if importAdder != nil {
			additionalTextEdits = new(importAdder.Edits())
		}

		return &lsproto.CompletionItem{
			Label:               name,
			Kind:                new(lsproto.CompletionItemKindSnippet),
			SortText:            new(string(SortTextGlobalsOrKeywords)),
			InsertText:          strPtrTo(insertText),
			AdditionalTextEdits: additionalTextEdits,
			InsertTextFormat:    core.IfElse(clientSupportsItemSnippet(ctx), new(lsproto.InsertTextFormatSnippet), nil),
			Data: &lsproto.CompletionItemData{
				FileName: file.FileName(),
				Position: int32(position),
				Name:     name,
				Source:   string(completionSourceSwitchCases),
			},
		}, nil
	}
	return nil, nil
}

func typeNodeToExpression(
	typeNode *ast.TypeNode,
	target core.ScriptTarget,
	quotePreference lsutil.QuotePreference,
	factory *ast.NodeFactory,
) *ast.Expression {
	switch typeNode.Kind {
	case ast.KindTypeReference:
		typeName := typeNode.AsTypeReferenceNode().TypeName
		return entityNameToExpression(typeName, target, quotePreference, factory)
	case ast.KindIndexedAccessType:
		objectExpression := typeNodeToExpression(
			typeNode.AsIndexedAccessTypeNode().ObjectType,
			target,
			quotePreference,
			factory,
		)
		indexExpression := typeNodeToExpression(
			typeNode.AsIndexedAccessTypeNode().IndexType,
			target,
			quotePreference,
			factory,
		)
		if objectExpression != nil && indexExpression != nil {
			return factory.NewElementAccessExpression(objectExpression, nil /*questionDotToken*/, indexExpression, ast.NodeFlagsNone)
		}
		return nil
	case ast.KindLiteralType:
		literal := typeNode.AsLiteralTypeNode().Literal
		switch literal.Kind {
		case ast.KindStringLiteral:
			expr := factory.NewStringLiteral(literal.Text(), core.IfElse(quotePreference == lsutil.QuotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone))
			return expr
		case ast.KindNumericLiteral:
			expr := factory.NewNumericLiteral(literal.Text(), literal.AsNumericLiteral().TokenFlags)
			return expr
		default:
			return nil
		}
	case ast.KindParenthesizedType:
		expr := typeNodeToExpression(
			typeNode.AsParenthesizedTypeNode().Type,
			target,
			quotePreference,
			factory,
		)
		if expr == nil {
			return nil
		}
		if ast.IsIdentifier(expr) {
			return expr
		}
		return factory.NewParenthesizedExpression(expr)
	case ast.KindTypeQuery:
		return entityNameToExpression(typeNode.AsTypeQueryNode().ExprName, target, quotePreference, factory)
	case ast.KindImportType:
		debug.Fail(`We should not get an import type after calling 'typeToAutoImportableTypeNode'.`)
		return nil
	}
	return nil
}

func entityNameToExpression(
	entityName *ast.EntityName,
	target core.ScriptTarget,
	quotePreference lsutil.QuotePreference,
	factory *ast.NodeFactory,
) *ast.Expression {
	if ast.IsIdentifier(entityName) {
		return entityName
	}
	return factory.NewPropertyAccessExpression(
		entityNameToExpression(entityName.AsQualifiedName().Left, target, quotePreference, factory),
		nil, /*questionDotToken*/
		entityName.AsQualifiedName().Right,
		ast.NodeFlagsNone,
	)
}

type snippetPrinter struct {
	baseWriter *printer.ChangeTrackerWriter
	printer    *printer.Printer
	writer     *snippetEmitTextWriter
	factory    *ast.NodeFactory
}

/** Snippet-escaping version of `printer.printNode`. */
func (p *snippetPrinter) printNode(node *ast.Node) string {
	unescaped := p.printUnescapedNode(node)
	if len(p.writer.escapes) > 0 {
		return core.ApplyBulkEdits(unescaped, p.writer.escapes)
	}
	return unescaped
}

func (p *snippetPrinter) printUnescapedNode(node *ast.Node) string {
	p.writer.escapes = nil
	p.writer.Clear()
	p.printer.Write(node, nil /*sourceFile*/, p.writer, nil /*sourceMapGenerator*/)
	return p.writer.String()
}

func (p *snippetPrinter) printAndFormatNode(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile) string {
	text := p.printUnescapedNode(node)
	nodeWithPos := p.baseWriter.AssignPositionsToNode(node, p.factory)
	syntheticFile := p.createSyntheticFile(nodeWithPos, text, sourceFile)
	changes := format.FormatNodeGivenIndentation(
		ctx,
		nodeWithPos,
		syntheticFile,
		sourceFile.LanguageVariant,
		0, /*initialIndentation*/
		0, /*delta*/
	)

	allChanges := changes
	if len(p.writer.escapes) > 0 {
		allChanges = append(changes, p.writer.escapes...)
		slices.SortFunc(allChanges, func(a, b core.TextChange) int { return core.CompareTextRanges(a.TextRange, b.TextRange) })
	}

	return core.ApplyBulkEdits(syntheticFile.Text(), allChanges)
}

// Creates a source file containing `node` for formatting purposes.
// `node` and descendants need to be synthetic nodes with positions assigned.
// This function also assigns parent pointers.
func (p *snippetPrinter) createSyntheticFile(node *ast.Node, text string, targetFile *ast.SourceFile) *ast.SourceFile {
	eof := p.factory.NewToken(ast.KindEndOfFile)
	eof.Loc = core.NewTextRange(len(text), len(text))
	statements := p.factory.NewNodeList([]*ast.Node{node})
	statements.Loc = core.NewTextRange(node.Pos(), node.End())
	syntheticFile := p.factory.NewSourceFile(
		targetFile.ParseOptions(),
		text,
		statements,
		eof,
	)
	syntheticFile.Loc = core.NewTextRange(0, len(text))
	ast.SetParentInChildren(syntheticFile)
	return syntheticFile.AsSourceFile()
}

func createSnippetPrinter(options printer.PrinterOptions) *snippetPrinter {
	baseWriter := printer.NewChangeTrackerWriter(options.NewLine.GetNewLineCharacter(), -1)
	printer := printer.NewPrinter(options, baseWriter.GetPrintHandlers(), nil /*emitContext*/)
	writer := &snippetEmitTextWriter{
		ChangeTrackerWriter: baseWriter,
	}
	return &snippetPrinter{
		baseWriter: baseWriter,
		printer:    printer,
		writer:     writer,
		factory:    ast.NewNodeFactory(ast.NodeFactoryHooks{}),
	}
}

// Override base writer methods to perform snippet escaping.
type snippetEmitTextWriter struct {
	*printer.ChangeTrackerWriter
	escapes []core.TextChange
}

func (w *snippetEmitTextWriter) nonEscapingWrite(s string) {
	w.ChangeTrackerWriter.Write(s)
}

func (w *snippetEmitTextWriter) Write(s string) {
	w.escapingWrite(s, func() { w.ChangeTrackerWriter.Write(s) })
}

func (w *snippetEmitTextWriter) WriteComment(text string) {
	w.escapingWrite(text, func() { w.ChangeTrackerWriter.WriteComment(text) })
}

func (w *snippetEmitTextWriter) WriteStringLiteral(text string) {
	w.escapingWrite(text, func() { w.ChangeTrackerWriter.WriteStringLiteral(text) })
}

func (w *snippetEmitTextWriter) WriteParameter(text string) {
	w.escapingWrite(text, func() { w.ChangeTrackerWriter.WriteParameter(text) })
}

func (w *snippetEmitTextWriter) WriteProperty(text string) {
	w.escapingWrite(text, func() { w.ChangeTrackerWriter.WriteProperty(text) })
}

func (w *snippetEmitTextWriter) WriteSymbol(text string, symbol *ast.Symbol) {
	w.escapingWrite(text, func() { w.ChangeTrackerWriter.WriteSymbol(text, symbol) })
}

// The formatter/scanner will have issues with snippet-escaped text,
// so instead of writing the escaped text directly to the writer,
// generate a set of changes that can be applied to the unescaped text
// to escape it post-formatting.
func (w *snippetEmitTextWriter) escapingWrite(s string, write func()) {
	escaped := escapeSnippetText(s)
	if escaped != s {
		start := w.GetTextPos()
		write()
		end := w.GetTextPos()
		w.escapes = append(
			w.escapes,
			core.TextChange{
				NewText:   escaped,
				TextRange: core.NewTextRange(start, end),
			},
		)
	} else {
		write()
	}
}
