kittenipc/kitcom/internal/tsgo/ls/string_completions.go
2025-10-15 10:12:44 +03:00

732 lines
22 KiB
Go

package ls
import (
"context"
"fmt"
"slices"
"strings"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
)
type completionsFromTypes struct {
types []*checker.StringLiteralType
isNewIdentifier bool
}
type completionsFromProperties struct {
symbols []*ast.Symbol
hasIndexSignature bool
}
type pathCompletion struct {
name string
// ScriptElementKindScriptElement | ScriptElementKindDirectory | ScriptElementKindExternalModuleName
kind ScriptElementKind
extension string
textRange *core.TextRange
}
type stringLiteralCompletions struct {
fromTypes *completionsFromTypes
fromProperties *completionsFromProperties
fromPaths []*pathCompletion
}
func (l *LanguageService) getStringLiteralCompletions(
ctx context.Context,
file *ast.SourceFile,
position int,
contextToken *ast.Node,
compilerOptions *core.CompilerOptions,
preferences *UserPreferences,
clientOptions *lsproto.CompletionClientCapabilities,
) *lsproto.CompletionList {
// !!! reference comment
if IsInString(file, position, contextToken) {
if contextToken == nil || !ast.IsStringLiteralLike(contextToken) {
return nil
}
entries := l.getStringLiteralCompletionEntries(
ctx,
file,
contextToken,
position,
preferences)
return l.convertStringLiteralCompletions(
ctx,
entries,
contextToken,
file,
position,
compilerOptions,
preferences,
clientOptions,
)
}
return nil
}
func (l *LanguageService) convertStringLiteralCompletions(
ctx context.Context,
completion *stringLiteralCompletions,
contextToken *ast.StringLiteralLike,
file *ast.SourceFile,
position int,
options *core.CompilerOptions,
preferences *UserPreferences,
clientOptions *lsproto.CompletionClientCapabilities,
) *lsproto.CompletionList {
if completion == nil {
return nil
}
optionalReplacementRange := l.createRangeFromStringLiteralLikeContent(file, contextToken, position)
switch {
case completion.fromPaths != nil:
completion := completion.fromPaths
return l.convertPathCompletions(completion, file, position, clientOptions)
case completion.fromProperties != nil:
completion := completion.fromProperties
data := &completionDataData{
symbols: completion.symbols,
completionKind: CompletionKindString,
isNewIdentifierLocation: completion.hasIndexSignature,
location: file.AsNode(),
contextToken: contextToken,
}
_, items := l.getCompletionEntriesFromSymbols(
ctx,
data,
contextToken, /*replacementToken*/
position,
file,
preferences,
options,
clientOptions,
)
defaultCommitCharacters := getDefaultCommitCharacters(completion.hasIndexSignature)
itemDefaults := l.setItemDefaults(
clientOptions,
position,
file,
items,
&defaultCommitCharacters,
optionalReplacementRange,
)
return &lsproto.CompletionList{
IsIncomplete: false,
ItemDefaults: itemDefaults,
Items: items,
}
case completion.fromTypes != nil:
completion := completion.fromTypes
var quoteChar printer.QuoteChar
if contextToken.Kind == ast.KindNoSubstitutionTemplateLiteral {
quoteChar = printer.QuoteCharBacktick
} else if strings.HasPrefix(contextToken.Text(), "'") {
quoteChar = printer.QuoteCharSingleQuote
} else {
quoteChar = printer.QuoteCharDoubleQuote
}
items := core.Map(completion.types, func(t *checker.StringLiteralType) *lsproto.CompletionItem {
name := printer.EscapeString(t.AsLiteralType().Value().(string), quoteChar)
return l.createLSPCompletionItem(
name,
"", /*insertText*/
"", /*filterText*/
SortTextLocationPriority,
ScriptElementKindString,
collections.Set[ScriptElementKindModifier]{},
l.getReplacementRangeForContextToken(file, contextToken, position),
nil, /*commitCharacters*/
nil, /*labelDetails*/
file,
position,
clientOptions,
false, /*isMemberCompletion*/
false, /*isSnippet*/
false, /*hasAction*/
false, /*preselect*/
"", /*source*/
nil, /*autoImportEntryData*/
)
})
defaultCommitCharacters := getDefaultCommitCharacters(completion.isNewIdentifier)
itemDefaults := l.setItemDefaults(
clientOptions,
position,
file,
items,
&defaultCommitCharacters,
nil, /*optionalReplacementSpan*/
)
return &lsproto.CompletionList{
IsIncomplete: false,
ItemDefaults: itemDefaults,
Items: items,
}
default:
return nil
}
}
func (l *LanguageService) convertPathCompletions(
pathCompletions []*pathCompletion,
file *ast.SourceFile,
position int,
clientOptions *lsproto.CompletionClientCapabilities,
) *lsproto.CompletionList {
isNewIdentifierLocation := true // The user may type in a path that doesn't yet exist, creating a "new identifier" with respect to the collection of identifiers the server is aware of.
defaultCommitCharacters := getDefaultCommitCharacters(isNewIdentifierLocation)
items := core.Map(pathCompletions, func(pathCompletion *pathCompletion) *lsproto.CompletionItem {
replacementSpan := l.createLspRangeFromBounds(pathCompletion.textRange.Pos(), pathCompletion.textRange.End(), file)
return l.createLSPCompletionItem(
pathCompletion.name,
"", /*insertText*/
"", /*filterText*/
SortTextLocationPriority,
pathCompletion.kind,
*collections.NewSetFromItems(kindModifiersFromExtension(pathCompletion.extension)),
replacementSpan,
nil, /*commitCharacters*/
nil, /*labelDetails*/
file,
position,
clientOptions,
false, /*isMemberCompletion*/
false, /*isSnippet*/
false, /*hasAction*/
false, /*preselect*/
"", /*source*/
nil, /*autoImportEntryData*/
)
})
itemDefaults := l.setItemDefaults(
clientOptions,
position,
file,
items,
&defaultCommitCharacters,
nil, /*optionalReplacementSpan*/
)
return &lsproto.CompletionList{
IsIncomplete: false,
ItemDefaults: itemDefaults,
Items: items,
}
}
func (l *LanguageService) getStringLiteralCompletionEntries(
ctx context.Context,
file *ast.SourceFile,
node *ast.StringLiteralLike,
position int,
preferences *UserPreferences,
) *stringLiteralCompletions {
typeChecker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file)
defer done()
parent := walkUpParentheses(node.Parent)
switch parent.Kind {
case ast.KindLiteralType:
grandparent := walkUpParentheses(parent.Parent)
if grandparent.Kind == ast.KindImportType {
return getStringLiteralCompletionsFromModuleNames(
file,
node,
l.GetProgram(),
preferences,
)
}
return fromUnionableLiteralType(grandparent, parent, position, typeChecker)
case ast.KindPropertyAssignment:
if ast.IsObjectLiteralExpression(parent.Parent) && parent.Name() == node {
// Get quoted name of properties of the object literal expression
// i.e. interface ConfigFiles {
// 'jspm:dev': string
// }
// let files: ConfigFiles = {
// '/*completion position*/'
// }
//
// function foo(c: ConfigFiles) {}
// foo({
// '/*completion position*/'
// });
return &stringLiteralCompletions{
fromProperties: stringLiteralCompletionsForObjectLiteral(typeChecker, parent.Parent),
}
}
result := fromContextualType(checker.ContextFlagsCompletions, node, typeChecker)
if result != nil {
return &stringLiteralCompletions{
fromTypes: result,
}
}
return &stringLiteralCompletions{
fromTypes: fromContextualType(checker.ContextFlagsNone, node, typeChecker),
}
case ast.KindElementAccessExpression:
expression := parent.Expression()
argumentExpression := parent.AsElementAccessExpression().ArgumentExpression
if node == ast.SkipParentheses(argumentExpression) {
// Get all names of properties on the expression
// i.e. interface A {
// 'prop1': string
// }
// let a: A;
// a['/*completion position*/']
t := typeChecker.GetTypeAtLocation(expression)
return &stringLiteralCompletions{
fromProperties: stringLiteralCompletionsFromProperties(t, typeChecker),
}
}
return nil
case ast.KindCallExpression, ast.KindNewExpression, ast.KindJsxAttribute:
if !isRequireCallArgument(node) && !ast.IsImportCall(parent) {
var argumentNode *ast.Node
if parent.Kind == ast.KindJsxAttribute {
argumentNode = parent.Parent
} else {
argumentNode = node
}
argumentInfo := getArgumentInfoForCompletions(argumentNode, position, file, typeChecker)
// Get string literal completions from specialized signatures of the target
// i.e. declare function f(a: 'A');
// f("/*completion position*/")
if argumentInfo == nil {
return nil
}
result := getStringLiteralCompletionsFromSignature(argumentInfo.invocation, node, argumentInfo, typeChecker)
if result != nil {
return &stringLiteralCompletions{
fromTypes: result,
}
}
return &stringLiteralCompletions{
fromTypes: fromContextualType(checker.ContextFlagsNone, node, typeChecker),
}
}
fallthrough // is `require("")` or `require(""` or `import("")`
case ast.KindImportDeclaration, ast.KindExportDeclaration, ast.KindExternalModuleReference, ast.KindJSDocImportTag:
// Get all known external module names or complete a path to a module
// i.e. import * as ns from "/*completion position*/";
// var y = import("/*completion position*/");
// import x = require("/*completion position*/");
// var y = require("/*completion position*/");
// export * from "/*completion position*/";
return getStringLiteralCompletionsFromModuleNames(file, node, l.GetProgram(), preferences)
case ast.KindCaseClause:
tracker := newCaseClauseTracker(typeChecker, parent.Parent.AsCaseBlock().Clauses.Nodes)
contextualTypes := fromContextualType(checker.ContextFlagsCompletions, node, typeChecker)
if contextualTypes == nil {
return nil
}
literals := core.Filter(contextualTypes.types, func(t *checker.StringLiteralType) bool {
return !tracker.hasValue(t.AsLiteralType().Value())
})
return &stringLiteralCompletions{
fromTypes: &completionsFromTypes{
types: literals,
isNewIdentifier: false,
},
}
case ast.KindImportSpecifier, ast.KindExportSpecifier:
// Complete string aliases in `import { "|" } from` and `export { "|" } from`
specifier := parent
if propertyName := specifier.PropertyName(); propertyName != nil && node != propertyName {
return nil // Don't complete in `export { "..." as "|" } from`
}
namedImportsOrExports := specifier.Parent
var moduleSpecifier *ast.Node
if namedImportsOrExports.Kind == ast.KindNamedImports {
moduleSpecifier = namedImportsOrExports.Parent.Parent
} else {
moduleSpecifier = namedImportsOrExports.Parent
}
if moduleSpecifier == nil {
return nil
}
moduleSpecifierSymbol := typeChecker.GetSymbolAtLocation(moduleSpecifier)
if moduleSpecifierSymbol == nil {
return nil
}
exports := typeChecker.GetExportsAndPropertiesOfModule(moduleSpecifierSymbol)
existing := collections.NewSetFromItems(core.Map(namedImportsOrExports.Elements(), func(n *ast.Node) string {
if n.PropertyName() != nil {
return n.PropertyName().Text()
}
return n.Name().Text()
})...)
uniques := core.Filter(exports, func(e *ast.Symbol) bool {
return e.Name != ast.InternalSymbolNameDefault && !existing.Has(e.Name)
})
return &stringLiteralCompletions{
fromProperties: &completionsFromProperties{
symbols: uniques,
hasIndexSignature: false,
},
}
case ast.KindBinaryExpression:
if parent.AsBinaryExpression().OperatorToken.Kind == ast.KindInKeyword {
t := typeChecker.GetTypeAtLocation(parent.AsBinaryExpression().Right)
properties := getPropertiesForCompletion(t, typeChecker)
return &stringLiteralCompletions{
fromProperties: &completionsFromProperties{
symbols: core.Filter(properties, func(s *ast.Symbol) bool {
return s.ValueDeclaration == nil || !ast.IsPrivateIdentifierClassElementDeclaration(s.ValueDeclaration)
}),
hasIndexSignature: false,
},
}
}
return &stringLiteralCompletions{
fromTypes: fromContextualType(checker.ContextFlagsNone, node, typeChecker),
}
default:
result := fromContextualType(checker.ContextFlagsCompletions, node, typeChecker)
if result != nil {
return &stringLiteralCompletions{
fromTypes: result,
}
}
return &stringLiteralCompletions{
fromTypes: fromContextualType(checker.ContextFlagsNone, node, typeChecker),
}
}
}
func fromContextualType(contextFlags checker.ContextFlags, node *ast.Node, typeChecker *checker.Checker) *completionsFromTypes {
// Get completion for string literal from string literal type
// i.e. var x: "hi" | "hello" = "/*completion position*/"
types := getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker, contextFlags), nil, typeChecker)
if len(types) == 0 {
return nil
}
return &completionsFromTypes{
types: types,
isNewIdentifier: false,
}
}
func fromUnionableLiteralType(
grandparent *ast.Node,
parent *ast.Node,
position int,
typeChecker *checker.Checker,
) *stringLiteralCompletions {
switch grandparent.Kind {
case ast.KindExpressionWithTypeArguments, ast.KindTypeReference:
typeArgument := ast.FindAncestor(parent, func(n *ast.Node) bool { return n.Parent == grandparent })
if typeArgument != nil {
t := typeChecker.GetTypeArgumentConstraint(typeArgument)
return &stringLiteralCompletions{
fromTypes: &completionsFromTypes{
types: getStringLiteralTypes(t, nil, typeChecker),
isNewIdentifier: false,
},
}
}
return nil
case ast.KindIndexedAccessType:
// Get all apparent property names
// i.e. interface Foo {
// foo: string;
// bar: string;
// }
// let x: Foo["/*completion position*/"]
indexType := grandparent.AsIndexedAccessTypeNode().IndexType
objectType := grandparent.AsIndexedAccessTypeNode().ObjectType
if !indexType.Loc.ContainsInclusive(position) {
return nil
}
t := typeChecker.GetTypeFromTypeNode(objectType)
return &stringLiteralCompletions{
fromProperties: stringLiteralCompletionsFromProperties(t, typeChecker),
}
case ast.KindUnionType:
result := fromUnionableLiteralType(
walkUpParentheses(grandparent.Parent),
parent,
position,
typeChecker)
if result == nil {
return nil
}
alreadyUsedTypes := getAlreadyUsedTypesInStringLiteralUnion(grandparent, parent)
switch {
case result.fromProperties != nil:
result := result.fromProperties
return &stringLiteralCompletions{
fromProperties: &completionsFromProperties{
symbols: core.Filter(
result.symbols,
func(s *ast.Symbol) bool { return !slices.Contains(alreadyUsedTypes, s.Name) },
),
hasIndexSignature: result.hasIndexSignature,
},
}
case result.fromTypes != nil:
result := result.fromTypes
return &stringLiteralCompletions{
fromTypes: &completionsFromTypes{
types: core.Filter(result.types, func(t *checker.StringLiteralType) bool {
return !slices.Contains(alreadyUsedTypes, t.AsLiteralType().Value().(string))
}),
isNewIdentifier: false,
},
}
default:
return nil
}
default:
return nil
}
}
func stringLiteralCompletionsForObjectLiteral(
typeChecker *checker.Checker,
objectLiteralExpression *ast.ObjectLiteralExpressionNode,
) *completionsFromProperties {
contextualType := typeChecker.GetContextualType(objectLiteralExpression, checker.ContextFlagsNone)
if contextualType == nil {
return nil
}
completionsType := typeChecker.GetContextualType(objectLiteralExpression, checker.ContextFlagsCompletions)
symbols := getPropertiesForObjectExpression(
contextualType,
completionsType,
objectLiteralExpression,
typeChecker)
return &completionsFromProperties{
symbols: symbols,
hasIndexSignature: hasIndexSignature(contextualType, typeChecker),
}
}
func stringLiteralCompletionsFromProperties(t *checker.Type, typeChecker *checker.Checker) *completionsFromProperties {
return &completionsFromProperties{
symbols: core.Filter(typeChecker.GetApparentProperties(t), func(s *ast.Symbol) bool {
return !(s.ValueDeclaration != nil && ast.IsPrivateIdentifierClassElementDeclaration(s.ValueDeclaration))
}),
hasIndexSignature: hasIndexSignature(t, typeChecker),
}
}
func getStringLiteralCompletionsFromModuleNames(
file *ast.SourceFile,
node *ast.LiteralExpression,
program *compiler.Program,
preferences *UserPreferences,
) *stringLiteralCompletions {
// !!! needs `getModeForUsageLocationWorker`
return nil
}
func walkUpParentheses(node *ast.Node) *ast.Node {
switch node.Kind {
case ast.KindParenthesizedType:
return ast.WalkUpParenthesizedTypes(node)
case ast.KindParenthesizedExpression:
return ast.WalkUpParenthesizedExpressions(node)
default:
return node
}
}
func getStringLiteralTypes(t *checker.Type, uniques *collections.Set[string], typeChecker *checker.Checker) []*checker.StringLiteralType {
if t == nil {
return nil
}
if uniques == nil {
uniques = &collections.Set[string]{}
}
t = skipConstraint(t, typeChecker)
if t.IsUnion() {
var types []*checker.StringLiteralType
for _, elementType := range t.Types() {
types = append(types, getStringLiteralTypes(elementType, uniques, typeChecker)...)
}
return types
}
if t.IsStringLiteral() && !t.IsEnumLiteral() && uniques.AddIfAbsent(t.AsLiteralType().Value().(string)) {
return []*checker.StringLiteralType{t}
}
return nil
}
func getAlreadyUsedTypesInStringLiteralUnion(union *ast.UnionType, current *ast.LiteralType) []string {
typesList := union.AsUnionTypeNode().Types
if typesList == nil {
return nil
}
var values []string
for _, typeNode := range typesList.Nodes {
if typeNode != current && ast.IsLiteralTypeNode(typeNode) &&
ast.IsStringLiteral(typeNode.AsLiteralTypeNode().Literal) {
values = append(values, typeNode.AsLiteralTypeNode().Literal.Text())
}
}
return values
}
func hasIndexSignature(t *checker.Type, typeChecker *checker.Checker) bool {
return typeChecker.GetStringIndexType(t) != nil || typeChecker.GetNumberIndexType(t) != nil
}
// Matches
//
// require(""
// require("")
func isRequireCallArgument(node *ast.Node) bool {
return ast.IsCallExpression(node.Parent) && len(node.Parent.Arguments()) > 0 && node.Parent.Arguments()[0] == node &&
ast.IsIdentifier(node.Parent.Expression()) && node.Parent.Expression().Text() == "require"
}
func kindModifiersFromExtension(extension string) ScriptElementKindModifier {
switch extension {
case tspath.ExtensionDts:
return ScriptElementKindModifierDts
case tspath.ExtensionJs:
return ScriptElementKindModifierJs
case tspath.ExtensionJson:
return ScriptElementKindModifierJson
case tspath.ExtensionJsx:
return ScriptElementKindModifierJsx
case tspath.ExtensionTs:
return ScriptElementKindModifierTs
case tspath.ExtensionTsx:
return ScriptElementKindModifierTsx
case tspath.ExtensionDmts:
return ScriptElementKindModifierDmts
case tspath.ExtensionMjs:
return ScriptElementKindModifierMjs
case tspath.ExtensionMts:
return ScriptElementKindModifierMts
case tspath.ExtensionDcts:
return ScriptElementKindModifierDcts
case tspath.ExtensionCjs:
return ScriptElementKindModifierCjs
case tspath.ExtensionCts:
return ScriptElementKindModifierCts
case tspath.ExtensionTsBuildInfo:
panic(fmt.Sprintf("Extension %v is unsupported.", tspath.ExtensionTsBuildInfo))
case "":
return ScriptElementKindModifierNone
default:
panic(fmt.Sprintf("Unexpected extension: %v", extension))
}
}
func getStringLiteralCompletionsFromSignature(
call *ast.CallLikeExpression,
arg *ast.StringLiteralLike,
argumentInfo *argumentInfoForCompletions,
typeChecker *checker.Checker,
) *completionsFromTypes {
isNewIdentifier := false
uniques := collections.Set[string]{}
var editingArgument *ast.Node
if ast.IsJsxOpeningLikeElement(call) {
editingArgument = ast.FindAncestor(arg.Parent, ast.IsJsxAttribute)
if editingArgument == nil {
panic("Expected jsx opening-like element to have a jsx attribute as ancestor.")
}
} else {
editingArgument = arg
}
candidates := typeChecker.GetCandidateSignaturesForStringLiteralCompletions(call, editingArgument)
var types []*checker.StringLiteralType
for _, candidate := range candidates {
if !candidate.HasRestParameter() && argumentInfo.argumentCount > len(candidate.Parameters()) {
continue
}
t := typeChecker.GetTypeParameterAtPosition(candidate, argumentInfo.argumentIndex)
if ast.IsJsxOpeningLikeElement(call) {
propType := typeChecker.GetTypeOfPropertyOfType(t, editingArgument.AsJsxAttribute().Name().Text())
if propType != nil {
t = propType
}
}
isNewIdentifier = isNewIdentifier || t.IsString()
types = append(types, getStringLiteralTypes(t, &uniques, typeChecker)...)
}
if len(types) > 0 {
return &completionsFromTypes{
types: types,
isNewIdentifier: isNewIdentifier,
}
}
return nil
}
func (l *LanguageService) getStringLiteralCompletionDetails(
ctx context.Context,
checker *checker.Checker,
item *lsproto.CompletionItem,
name string,
file *ast.SourceFile,
position int,
contextToken *ast.Node,
preferences *UserPreferences,
) *lsproto.CompletionItem {
if contextToken == nil || !ast.IsStringLiteralLike(contextToken) {
return item
}
completions := l.getStringLiteralCompletionEntries(
ctx,
file,
contextToken,
position,
preferences,
)
if completions == nil {
return item
}
return stringLiteralCompletionDetails(item, name, contextToken, completions, file, checker)
}
func stringLiteralCompletionDetails(
item *lsproto.CompletionItem,
name string,
location *ast.Node,
completion *stringLiteralCompletions,
file *ast.SourceFile,
checker *checker.Checker,
) *lsproto.CompletionItem {
switch {
case completion.fromPaths != nil:
pathCompletions := completion.fromPaths
for _, pathCompletion := range pathCompletions {
if pathCompletion.name == name {
return createCompletionDetails(item, name, "" /*documentation*/)
}
}
case completion.fromProperties != nil:
properties := completion.fromProperties
for _, symbol := range properties.symbols {
if symbol.Name == name {
return createCompletionDetailsForSymbol(item, symbol, checker, location, nil /*actions*/)
}
}
case completion.fromTypes != nil:
types := completion.fromTypes
for _, t := range types.types {
if t.AsLiteralType().Value().(string) == name {
return createCompletionDetails(item, name, "" /*documentation*/)
}
}
}
return item
}