Files
kjol/tools/tsgo/internal/ls/string_completions.go
2026-07-09 16:50:43 -04:00

2212 lines
71 KiB
Go

package ls
import (
"cmp"
"context"
"fmt"
"iter"
"maps"
"slices"
"strings"
"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/ls/lsutil"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/modulespecifiers"
"github.com/microsoft/typescript-go/internal/packagejson"
"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/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
type completionsFromTypes struct {
types []*checker.StringLiteralType
isNewIdentifier bool
}
type completionsFromProperties struct {
symbols []*ast.Symbol
hasIndexSignature bool
}
type pathCompletion struct {
name string
kind lsutil.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,
checker *checker.Checker,
compilerOptions *core.CompilerOptions,
includeSymbols bool,
) *CompletionList {
if isInReferenceComment(file, position) {
entries := l.getTripleSlashReferenceCompletions(file, position, l.GetProgram(), checker)
return l.convertPathCompletions(ctx, entries, file, position)
}
if IsInString(file, position, contextToken) {
if contextToken == nil || !ast.IsStringLiteralLike(contextToken) {
return nil
}
entries := l.getStringLiteralCompletionEntries(
ctx,
file,
contextToken,
position,
checker,
)
return l.convertStringLiteralCompletions(
ctx,
entries,
contextToken,
file,
position,
checker,
compilerOptions,
includeSymbols,
)
}
return nil
}
func (l *LanguageService) convertStringLiteralCompletions(
ctx context.Context,
completion *stringLiteralCompletions,
contextToken *ast.StringLiteralLike,
file *ast.SourceFile,
position int,
typeChecker *checker.Checker,
options *core.CompilerOptions,
includeSymbols bool,
) *CompletionList {
if completion == nil {
return nil
}
optionalReplacementRange := l.createRangeFromStringLiteralLikeContent(file, contextToken, position)
switch {
case completion.fromPaths != nil:
completion := completion.fromPaths
return l.convertPathCompletions(ctx, completion, file, position)
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,
typeChecker,
data,
contextToken, /*replacementToken*/
position,
file,
options,
includeSymbols, /*includeSymbols*/
)
defaultCommitCharacters := getDefaultCommitCharacters(completion.hasIndexSignature)
itemDefaults := l.setItemDefaults(
ctx,
position,
file,
items,
&defaultCommitCharacters,
optionalReplacementRange,
)
return &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) *CompletionItem {
name := printer.EscapeString(t.AsLiteralType().Value().(string), quoteChar)
lspItem := l.createLSPCompletionItem(
ctx,
name,
"", /*insertText*/
"", /*filterText*/
SortTextLocationPriority,
lsutil.ScriptElementKindString,
lsutil.ScriptElementKindModifierNone,
l.getReplacementRangeForContextToken(file, contextToken, position),
nil, /*commitCharacters*/
nil, /*labelDetails*/
file,
position,
false, /*isMemberCompletion*/
false, /*isSnippet*/
false, /*hasAction*/
false, /*preselect*/
"", /*source*/
nil, /*autoImportEntryData*/
nil, /*detail*/
)
return &CompletionItem{
CompletionItem: lspItem,
}
})
defaultCommitCharacters := getDefaultCommitCharacters(completion.isNewIdentifier)
itemDefaults := l.setItemDefaults(
ctx,
position,
file,
items,
&defaultCommitCharacters,
nil, /*optionalReplacementSpan*/
)
return &CompletionList{
IsIncomplete: false,
ItemDefaults: itemDefaults,
Items: items,
}
default:
return nil
}
}
func (l *LanguageService) convertPathCompletions(
ctx context.Context,
pathCompletions []*pathCompletion,
file *ast.SourceFile,
position int,
) *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) *CompletionItem {
var replacementSpan *lsproto.Range
if pathCompletion.textRange != nil {
replacementSpan = new(l.createLspRangeFromBounds(pathCompletion.textRange.Pos(), pathCompletion.textRange.End(), file))
}
detail := pathCompletion.name
if !strings.HasSuffix(pathCompletion.name, pathCompletion.extension) {
detail += pathCompletion.extension
}
lspItem := l.createLSPCompletionItem(
ctx,
pathCompletion.name,
"", /*insertText*/
"", /*filterText*/
SortTextLocationPriority,
pathCompletion.kind,
kindModifiersFromExtension(pathCompletion.extension),
replacementSpan,
nil, /*commitCharacters*/
nil, /*labelDetails*/
file,
position,
false, /*isMemberCompletion*/
false, /*isSnippet*/
false, /*hasAction*/
false, /*preselect*/
"", /*source*/
nil, /*autoImportEntryData*/
&detail,
)
return &CompletionItem{
CompletionItem: lspItem,
}
})
itemDefaults := l.setItemDefaults(
ctx,
position,
file,
items,
&defaultCommitCharacters,
nil, /*optionalReplacementSpan*/
)
return &CompletionList{
IsIncomplete: false,
ItemDefaults: itemDefaults,
Items: items,
}
}
func (l *LanguageService) getStringLiteralCompletionEntries(
ctx context.Context,
file *ast.SourceFile,
node *ast.StringLiteralLike,
position int,
typeChecker *checker.Checker,
) *stringLiteralCompletions {
parent := walkUpParentheses(node.Parent)
switch parent.Kind {
case ast.KindLiteralType:
grandparent := walkUpParentheses(parent.Parent)
if grandparent.Kind == ast.KindImportType {
return l.getStringLiteralCompletionsFromModuleNames(
file,
node,
l.GetProgram(),
typeChecker,
)
}
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),
}
}
if ast.FindAncestor(parent.Parent, ast.IsCallLikeExpression) != nil {
uniques := &collections.Set[string]{}
stringLiteralTypes := append(
getStringLiteralTypes(typeChecker.GetContextualType(node, checker.ContextFlagsNone), uniques, typeChecker),
getStringLiteralTypes(typeChecker.GetContextualType(node, checker.ContextFlagsIgnoreNodeInferences), uniques, typeChecker)...,
)
return toStringLiteralCompletionsFromTypes(stringLiteralTypes)
}
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 l.getStringLiteralCompletionsFromModuleNames(file, node, l.GetProgram(), typeChecker)
case ast.KindCaseClause:
tracker := newCaseClauseTracker(typeChecker, parent.Parent.AsCaseBlock().Clauses.Nodes)
contextualTypes := fromContextualType(checker.ContextFlagsIgnoreNodeInferences, 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 {
return n.PropertyNameOrName().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.ContextFlagsIgnoreNodeInferences, 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*/"
return toCompletionsFromTypes(getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker, contextFlags), nil, typeChecker))
}
func toCompletionsFromTypes(types []*checker.StringLiteralType) *completionsFromTypes {
if len(types) == 0 {
return nil
}
return &completionsFromTypes{
types: types,
isNewIdentifier: false,
}
}
func toStringLiteralCompletionsFromTypes(types []*checker.StringLiteralType) *stringLiteralCompletions {
result := toCompletionsFromTypes(types)
if result == nil {
return nil
}
return &stringLiteralCompletions{
fromTypes: result,
}
}
func fromUnionableLiteralType(
grandparent *ast.Node,
parent *ast.Node,
position int,
typeChecker *checker.Checker,
) *stringLiteralCompletions {
switch grandparent.Kind {
case ast.KindCallExpression,
ast.KindExpressionWithTypeArguments,
ast.KindJsxOpeningElement,
ast.KindJsxSelfClosingElement,
ast.KindNewExpression,
ast.KindTaggedTemplateExpression,
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
}
case ast.KindPropertySignature:
return &stringLiteralCompletions{
fromTypes: &completionsFromTypes{
types: getStringLiteralTypes(getConstraintOfTypeArgumentProperty(grandparent, typeChecker), nil, typeChecker),
isNewIdentifier: false,
},
}
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.ContextFlagsIgnoreNodeInferences)
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 (l *LanguageService) getStringLiteralCompletionsFromModuleNames(
file *ast.SourceFile,
node *ast.LiteralExpression,
program *compiler.Program,
checker *checker.Checker,
) *stringLiteralCompletions {
nameAndKinds := l.getStringLiteralCompletionsFromModuleNamesWorker(
file,
node,
program,
checker,
)
textStart := astnav.GetStartOfNode(node, file, false /*includeJSDoc*/) + 1
return &stringLiteralCompletions{
fromPaths: addReplacementSpans(node.Text(), textStart, nameAndKinds),
}
}
func addReplacementSpans(text string, textStart int, names []moduleCompletionNameAndKind) []*pathCompletion {
textRange := getDirectoryFragmentRange(text, textStart)
return core.Map(names, func(nameAndKind moduleCompletionNameAndKind) *pathCompletion {
return &pathCompletion{
name: nameAndKind.name,
kind: moduletToScriptElementKind(nameAndKind.kind),
extension: nameAndKind.extension,
textRange: textRange,
}
})
}
func moduletToScriptElementKind(kind moduleCompletionKind) lsutil.ScriptElementKind {
switch kind {
case moduleCompletionKindDirectory:
return lsutil.ScriptElementKindDirectory
case moduleCompletionKindFile:
return lsutil.ScriptElementKindScriptElement
case moduleCompletionKindExternalModuleName:
return lsutil.ScriptElementKindExternalModuleName
}
panic(fmt.Sprintf("Unknown moduleCompletionKind: %d", kind))
}
func isAnyDirectorySeparator(r rune) bool {
return r == '/' || r == '\\'
}
// Replace everything after the last directory separator that appears
func getDirectoryFragmentRange(text string, textStart int) *core.TextRange {
index := strings.LastIndexFunc(text, isAnyDirectorySeparator)
var offset int
if index != -1 {
offset = index + 1
}
length := len(text) - offset
if length == 0 {
return nil
}
return new(core.NewTextRange(textStart+offset, textStart+offset+length))
}
func (l *LanguageService) getStringLiteralCompletionsFromModuleNamesWorker(
file *ast.SourceFile,
node *ast.LiteralExpression,
program *compiler.Program,
checker *checker.Checker,
) []moduleCompletionNameAndKind {
literalValue := tspath.NormalizeSlashes(node.Text())
var mode core.ResolutionMode
if ast.IsStringLiteralLike(node) {
mode = program.GetModeForUsageLocation(file, node)
}
scriptPath := file.Path()
scriptDirectory := scriptPath.GetDirectoryPath()
options := program.Options()
extensionOptions := l.getExtensionOptions(options, referenceKindModuleSpecifier, file, mode, checker)
if isPathRelativeToScript(literalValue) ||
(options.Paths.Size() == 0 && (tspath.IsRootedDiskPath(literalValue) || tspath.IsUrl(literalValue))) {
return l.getCompletionEntriesForRelativeModules(
literalValue,
string(scriptDirectory),
program,
scriptPath,
extensionOptions,
)
} else {
return l.getCompletionEntriesForNonRelativeModules(
literalValue,
string(scriptDirectory),
mode,
program,
checker,
extensionOptions,
)
}
}
// Check all of the declared modules and those in node modules. Possible sources of modules:
//
// Modules that are found by the type checker
// Modules found via patterns from "paths" compiler option
// Modules from node_modules (i.e. those listed in package.json)
// This includes all files that are found in node_modules/moduleName/ with acceptable file extensions
func (l *LanguageService) getCompletionEntriesForNonRelativeModules(
fragment string,
scriptPath string,
mode core.ResolutionMode,
program *compiler.Program,
typeChecker *checker.Checker,
extensionOptions *extensionOptions,
) []moduleCompletionNameAndKind {
compilerOptions := program.Options()
paths := compilerOptions.Paths
result := &moduleCompletionNameAndKindSet{names: map[string]moduleCompletionNameAndKind{}}
moduleResolution := compilerOptions.GetModuleResolutionKind()
if paths != nil && paths.Size() > 0 {
absolute := compilerOptions.GetPathsBasePath(program.GetCurrentDirectory())
l.addCompletionEntriesFromPaths(result, program, fragment, absolute, extensionOptions, paths)
}
fragmentDirectory := getFragmentDirectory(fragment)
for _, ambientName := range getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker) {
result.add(moduleCompletionNameAndKind{
name: ambientName,
kind: moduleCompletionKindExternalModuleName,
})
}
l.getCompletionEntriesFromTypings(program, scriptPath, fragmentDirectory, extensionOptions, result)
if moduleResolutionUsesNodeModules(moduleResolution) {
// If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies.
// (But do if we didn't find anything, e.g. 'package.json' missing.)
foundGlobal := false
if fragmentDirectory == "" {
for _, moduleName := range l.enumerateNodeModulesVisibleToScript(scriptPath) {
moduleResult := moduleCompletionNameAndKind{
name: moduleName,
kind: moduleCompletionKindExternalModuleName,
}
if _, has := result.names[moduleResult.name]; !has {
foundGlobal = true
result.add(moduleResult)
}
}
}
if !foundGlobal {
resolvePackageJsonExports := compilerOptions.GetResolvePackageJsonExports()
resolvePackageJsonImports := compilerOptions.GetResolvePackageJsonImports()
seenPackageScope := false
conditions := module.GetConditions(compilerOptions, mode)
// Returns true if the search should stop.
exportsOrImportsLookup := func(lookupTable *packagejson.ExportsOrImports, fragment string, baseDirectory string, isExports bool, isImports bool) bool {
if lookupTable == nil || lookupTable.Type != packagejson.JSONValueTypeObject {
return lookupTable != nil && lookupTable.Type != packagejson.JSONValueTypeNotPresent
}
keys := lookupTable.AsObject().Keys()
l.addCompletionEntriesFromPathsOrExportsOrImports(
result,
program,
isExports,
isImports,
fragment,
baseDirectory,
extensionOptions,
keys,
func(key string) []string {
keyValue, ok := lookupTable.AsObject().Get(key)
if !ok {
return nil
}
pattern := getPatternFromFirstMatchingCondition(&keyValue, conditions)
if pattern == "" {
return nil
}
if strings.HasSuffix(key, "/") && strings.HasSuffix(pattern, "/") {
return []string{pattern + "*"}
}
return []string{pattern}
},
module.ComparePatternKeys,
)
return true
}
importsLookup := func(directory string) {
if resolvePackageJsonImports && !seenPackageScope {
packageFile := tspath.CombinePaths(directory, "package.json")
packageJsonInfo := program.GetPackageJsonInfo(packageFile)
if packageJsonInfo != nil && packageJsonInfo.Exists() {
seenPackageScope = true
exportsOrImportsLookup(&packageJsonInfo.Contents.Imports, fragment, directory, false /*isExports*/, true /*isImports*/)
}
}
}
ancestorLookup := func(ancestor string) (any, bool) {
nodeModules := tspath.CombinePaths(ancestor, "node_modules")
if l.host.DirectoryExists(nodeModules) {
l.getCompletionEntriesForDirectoryFragment(
fragment,
nodeModules,
extensionOptions,
program,
false, /* moduleSpecifierIsRelative */
"",
result,
)
}
importsLookup(ancestor)
return nil, false
}
if fragmentDirectory != "" && resolvePackageJsonExports {
nodeModulesDirectoryOrImportsLookup := ancestorLookup
ancestorLookup = func(ancestor string) (any, bool) {
components := tspath.GetPathComponents(fragment, "")
components = components[1:] // shift off empty root
if len(components) == 0 {
nodeModulesDirectoryOrImportsLookup(ancestor)
return nil, false
}
packagePath := components[0]
components = components[1:]
if strings.HasPrefix(packagePath, "@") {
if len(components) == 0 {
nodeModulesDirectoryOrImportsLookup(ancestor)
return nil, false
}
subName := components[0]
components = components[1:]
packagePath = tspath.CombinePaths(packagePath, subName)
}
if resolvePackageJsonImports && strings.HasPrefix(packagePath, "#") {
importsLookup(ancestor)
return nil, false
}
packageDirectory := tspath.CombinePaths(ancestor, "node_modules", packagePath)
packageFile := tspath.CombinePaths(packageDirectory, "package.json")
packageJsonInfo := program.GetPackageJsonInfo(packageFile)
if packageJsonInfo != nil && packageJsonInfo.Exists() {
fragmentSubpath := strings.Join(components, "/")
if len(components) > 0 && tspath.HasTrailingDirectorySeparator(fragment) {
fragmentSubpath += "/"
}
if exportsOrImportsLookup(
&packageJsonInfo.Contents.Exports,
fragmentSubpath,
packageDirectory,
true, /*isExports*/
false, /*isImports*/
) {
return nil, false
}
}
nodeModulesDirectoryOrImportsLookup(ancestor)
return nil, false
}
}
globalCacheLocation := program.GetGlobalTypingsCacheLocation()
tspath.ForEachAncestorDirectoryStoppingAtGlobalCache(globalCacheLocation, scriptPath, ancestorLookup)
}
}
return slices.Collect(maps.Values(result.names))
}
func getFragmentDirectory(fragment string) string {
if !containsSlash(fragment) {
return ""
}
if tspath.HasTrailingDirectorySeparator(fragment) {
return fragment
}
return tspath.GetDirectoryPath(fragment)
}
func getPatternFromFirstMatchingCondition(target *packagejson.ExportsOrImports, conditions []string) string {
if target.Type == packagejson.JSONValueTypeString {
return target.AsString()
}
if target.Type == packagejson.JSONValueTypeObject {
obj := target.AsObject()
for condition := range obj.Keys() {
if condition == "default" || slices.Contains(conditions, condition) ||
(slices.Contains(conditions, "types") && module.IsApplicableVersionedTypesKey(condition)) {
pattern, ok := obj.Get(condition)
if ok {
return getPatternFromFirstMatchingCondition(&pattern, conditions)
}
}
}
}
return ""
}
func getAmbientModuleCompletions(fragment string, fragmentDirectory string, typeChecker *checker.Checker) []string {
ambientModules := typeChecker.GetAmbientModules()
var nonRelativeModuleNames []string
for _, sym := range ambientModules {
moduleName := stringutil.StripQuotes(sym.Name)
if strings.HasPrefix(moduleName, fragment) && !strings.Contains(moduleName, "*") {
nonRelativeModuleNames = append(nonRelativeModuleNames, moduleName)
}
}
if fragmentDirectory != "" {
moduleNameWithSeparator := tspath.EnsureTrailingDirectorySeparator(fragmentDirectory)
for i, moduleName := range nonRelativeModuleNames {
nonRelativeModuleNames[i] = strings.TrimPrefix(moduleName, moduleNameWithSeparator)
}
}
return nonRelativeModuleNames
}
func (l *LanguageService) getCompletionEntriesFromTypings(
program *compiler.Program,
scriptPath string,
fragmentDirectory string,
extensionOptions *extensionOptions,
result *moduleCompletionNameAndKindSet,
) {
options := program.Options()
seen := make(map[string]bool)
typeRoots, _ := options.GetEffectiveTypeRoots(program.GetCurrentDirectory())
for _, root := range typeRoots {
l.getCompletionEntriesFromTypingsDirectories(root, options, fragmentDirectory, extensionOptions, program, seen, result)
}
globalCacheLocation := program.GetGlobalTypingsCacheLocation()
tspath.ForEachAncestorDirectoryStoppingAtGlobalCache(globalCacheLocation, scriptPath, func(directory string) (any, bool) {
typesDir := tspath.CombinePaths(directory, "node_modules/@types")
l.getCompletionEntriesFromTypingsDirectories(typesDir, options, fragmentDirectory, extensionOptions, program, seen, result)
return nil, false
})
}
func (l *LanguageService) getCompletionEntriesFromTypingsDirectories(
directory string,
options *core.CompilerOptions,
fragmentDirectory string,
extensionOptions *extensionOptions,
program *compiler.Program,
seen map[string]bool,
result *moduleCompletionNameAndKindSet,
) {
if !l.host.DirectoryExists(directory) {
return
}
for _, typeDirectoryName := range l.GetDirectories(directory) {
packageName := module.UnmangleScopedPackageName(typeDirectoryName)
if len(options.Types) > 0 && !slices.Contains(options.Types, packageName) {
continue
}
if fragmentDirectory == "" {
if !seen[packageName] {
result.add(moduleCompletionNameAndKind{
name: packageName,
kind: moduleCompletionKindExternalModuleName,
})
seen[packageName] = true
}
} else {
baseDirectory := tspath.CombinePaths(directory, typeDirectoryName)
remainingFragment := tryRemoveDirectoryPrefix(fragmentDirectory, packageName, program.UseCaseSensitiveFileNames())
if remainingFragment != nil {
l.getCompletionEntriesForDirectoryFragment(
*remainingFragment,
baseDirectory,
extensionOptions,
program,
false,
"",
result,
)
}
}
}
}
func tryRemoveDirectoryPrefix(path string, prefix string, useCaseSensitiveFileNames bool) *string {
canonicalPath := tspath.GetCanonicalFileName(path, useCaseSensitiveFileNames)
canonicalPrefix := tspath.GetCanonicalFileName(prefix, useCaseSensitiveFileNames)
if strings.HasPrefix(canonicalPath, canonicalPrefix) {
withoutPrefix := path[len(prefix):]
if strings.HasPrefix(withoutPrefix, "/") || strings.HasPrefix(withoutPrefix, "\\") {
withoutPrefix = withoutPrefix[1:]
}
return &withoutPrefix
}
return nil
}
func (l *LanguageService) enumerateNodeModulesVisibleToScript(scriptPath string) []string {
var result []string
globalCacheLocation := l.program.GetGlobalTypingsCacheLocation()
tspath.ForEachAncestorDirectoryStoppingAtGlobalCache(globalCacheLocation, scriptPath, func(directory string) (any, bool) {
packageJsonPath := tspath.CombinePaths(directory, "package.json")
packageJsonInfo := l.program.GetPackageJsonInfo(packageJsonPath)
if packageJsonInfo != nil && packageJsonInfo.Exists() && packageJsonInfo.Contents != nil {
packageJsonInfo.Contents.RangeDependencies(func(name, version, dependencyField string) bool {
if !strings.HasPrefix(name, "@types/") {
result = append(result, name)
}
return true
})
}
return nil, false
})
return result
}
func (l *LanguageService) getExtensionOptions(
options *core.CompilerOptions,
referenceKind referenceKind,
file *ast.SourceFile,
mode core.ResolutionMode,
checker *checker.Checker,
) *extensionOptions {
extensionsToSearch := getSupportedExtensionsForModuleResolution(options, checker)
return &extensionOptions{
extensionsToSearch: extensionsToSearch,
referenceKind: referenceKind,
importingSourceFile: file,
endingPreference: l.UserPreferences().ImportModuleSpecifierEnding,
resolutionMode: mode,
}
}
func getSupportedExtensionsForModuleResolution(options *core.CompilerOptions, checker *checker.Checker) []string {
/** file extensions from ambient modules declarations e.g. *.css */
var extensions []string
if checker != nil {
ambientModules := checker.GetAmbientModules()
for _, module := range ambientModules {
name := stringutil.StripQuotes(module.Name)
if !strings.HasPrefix(name, "*.") || strings.Contains(name, "/") {
continue
}
extensions = append(extensions, name[1:])
}
}
supportedExtensions := tsoptions.GetSupportedExtensions(options, nil /*extraFileExtensions*/)
for _, ext := range supportedExtensions {
extensions = append(extensions, ext...)
}
moduleResolution := options.GetModuleResolutionKind()
if moduleResolutionUsesNodeModules(moduleResolution) {
return core.Flatten(tsoptions.GetSupportedExtensionsWithJsonIfResolveJsonModule(options, [][]string{extensions}))
}
return extensions
}
func moduleResolutionUsesNodeModules(moduleResolution core.ModuleResolutionKind) bool {
return moduleResolution >= core.ModuleResolutionKindNode16 && moduleResolution <= core.ModuleResolutionKindNodeNext ||
moduleResolution == core.ModuleResolutionKindBundler
}
// Returns true if the path is explicitly relative (i.e. relative to . or ..)
func isPathRelativeToScript(path string) bool {
return strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../")
}
func (l *LanguageService) getCompletionEntriesForRelativeModules(
literalValue string,
scriptDirectory string,
program *compiler.Program,
scriptPath tspath.Path,
extensionOptions *extensionOptions,
) []moduleCompletionNameAndKind {
options := program.Options()
if len(options.RootDirs) > 0 {
return l.getCompletionEntriesForDirectoryFragmentWithRootDirs(
options.RootDirs,
literalValue,
scriptDirectory,
program,
string(scriptPath),
extensionOptions,
)
} else {
result := l.getCompletionEntriesForDirectoryFragment(
literalValue,
scriptDirectory,
extensionOptions,
program,
true, /*moduleSpecifierIsRelative*/
string(scriptPath),
&moduleCompletionNameAndKindSet{names: map[string]moduleCompletionNameAndKind{}},
)
return slices.Collect(maps.Values(result.names))
}
}
func (l *LanguageService) getCompletionEntriesForDirectoryFragmentWithRootDirs(
rootDirs []string,
fragment string,
scriptDirectory string,
program *compiler.Program,
exclude string,
extensionOptions *extensionOptions,
) []moduleCompletionNameAndKind {
options := program.Options()
var basePath string
if options.Project != "" {
basePath = options.Project
} else {
basePath = program.GetCurrentDirectory()
}
ignoreCase := !program.UseCaseSensitiveFileNames()
baseDirectories := getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptDirectory, ignoreCase)
var allCompletions []moduleCompletionNameAndKind
for _, baseDirectory := range baseDirectories {
result := l.getCompletionEntriesForDirectoryFragment(
fragment,
baseDirectory,
extensionOptions,
program,
true, /*moduleSpecifierIsRelative*/
exclude,
&moduleCompletionNameAndKindSet{names: map[string]moduleCompletionNameAndKind{}},
)
for _, entry := range result.names {
allCompletions = append(allCompletions, entry)
}
}
// Deduplicate based on name, kind, and extension
return deduplicateModuleCompletions(allCompletions)
}
// getBaseDirectoriesFromRootDirs takes a script path and returns paths for all potential folders
// that could be merged with its containing folder via the "rootDirs" compiler option.
func getBaseDirectoriesFromRootDirs(rootDirs []string, basePath string, scriptDirectory string, ignoreCase bool) []string {
// Make all paths absolute/normalized if they are not already
normalizedRootDirs := make([]string, len(rootDirs))
for i, rootDirectory := range rootDirs {
var normalizedPath string
if tspath.IsRootedDiskPath(rootDirectory) {
normalizedPath = rootDirectory
} else {
normalizedPath = tspath.CombinePaths(basePath, rootDirectory)
}
normalizedRootDirs[i] = tspath.EnsureTrailingDirectorySeparator(tspath.NormalizePath(normalizedPath))
}
// Determine the path to the directory containing the script relative to the root directory it is contained within
var relativeDirectory string
comparePathsOptions := tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: !ignoreCase,
CurrentDirectory: basePath,
}
for _, rootDirectory := range normalizedRootDirs {
if tspath.ContainsPath(rootDirectory, scriptDirectory, comparePathsOptions) {
if len(rootDirectory) > len(scriptDirectory) {
relativeDirectory = ""
} else {
relativeDirectory = scriptDirectory[len(rootDirectory):]
}
break
}
}
// Now find a path for each potential directory that is to be merged with the one containing the script
var directories []string
for _, rootDirectory := range normalizedRootDirs {
directories = append(directories, tspath.RemoveTrailingDirectorySeparator(tspath.CombinePaths(rootDirectory, relativeDirectory)))
}
directories = append(directories, tspath.RemoveTrailingDirectorySeparator(scriptDirectory))
return deduplicateStrings(directories)
}
func deduplicateStrings(slice []string) []string {
if len(slice) <= 1 {
return slice
}
seen := make(map[string]bool)
var result []string
for _, s := range slice {
if !seen[s] {
seen[s] = true
result = append(result, s)
}
}
return result
}
func deduplicateModuleCompletions(completions []moduleCompletionNameAndKind) []moduleCompletionNameAndKind {
if len(completions) <= 1 {
return completions
}
type key struct {
name string
kind moduleCompletionKind
extension string
}
seen := make(map[key]bool)
var result []moduleCompletionNameAndKind
for _, c := range completions {
k := key{name: c.name, kind: c.kind, extension: c.extension}
if !seen[k] {
seen[k] = true
result = append(result, c)
}
}
return result
}
type moduleCompletionKind int
const (
moduleCompletionKindDirectory moduleCompletionKind = iota
moduleCompletionKindFile
moduleCompletionKindExternalModuleName
)
type moduleCompletionNameAndKind struct {
name string
kind moduleCompletionKind
extension string
}
type moduleCompletionNameAndKindSet struct {
names map[string]moduleCompletionNameAndKind
}
func (s *moduleCompletionNameAndKindSet) add(entry moduleCompletionNameAndKind) {
existing, ok := s.names[entry.name]
if !ok || existing.kind < entry.kind {
s.names[entry.name] = entry
}
}
type extensionOptions struct {
extensionsToSearch []string
referenceKind referenceKind
importingSourceFile *ast.SourceFile
endingPreference modulespecifiers.ImportModuleSpecifierEndingPreference
resolutionMode core.ResolutionMode
}
type referenceKind int
const (
referenceKindFileName referenceKind = iota
referenceKindModuleSpecifier
)
// Given a path ending at a directory, gets the completions for the path.
func (l *LanguageService) getCompletionEntriesForDirectoryFragment(
fragment string,
scriptDirectory string,
extensionOptions *extensionOptions,
program *compiler.Program,
moduleSpecifierIsRelative bool,
exclude string,
result *moduleCompletionNameAndKindSet,
) *moduleCompletionNameAndKindSet {
fragment = tspath.NormalizeSlashes(fragment)
// Remove the basename from the path.
// We don't use the basename to filter completions: the client is responsible for that filtering.
if !tspath.HasTrailingDirectorySeparator(fragment) {
fragment = tspath.GetDirectoryPath(fragment)
}
if fragment == "" {
fragment = "."
}
fragment = tspath.EnsureTrailingDirectorySeparator(fragment)
baseDirectory := tspath.ResolvePath(scriptDirectory, fragment)
if !moduleSpecifierIsRelative {
// Check for a version redirect.
packageJsonDirectory := program.GetNearestAncestorDirectoryWithPackageJson(baseDirectory)
if packageJsonDirectory != "" {
packageJsonPath := tspath.CombinePaths(packageJsonDirectory, "package.json")
packageJsonInfo := program.GetPackageJsonInfo(packageJsonPath)
if packageJsonInfo != nil && packageJsonInfo.Contents != nil &&
packageJsonInfo.Contents.TypesVersions.Type == packagejson.JSONValueTypeObject {
versionPaths := packageJsonInfo.Contents.GetVersionPaths(nil)
paths := versionPaths.GetPaths()
if paths.Size() > 0 {
pathInPackage := baseDirectory[len(tspath.EnsureTrailingDirectorySeparator(packageJsonDirectory)):]
if l.addCompletionEntriesFromPaths(result, program, pathInPackage, packageJsonDirectory, extensionOptions, paths) {
// One of the `versionPaths` was matched, which will block relative resolution
// to files and folders from here.
// All reachable paths given the pattern match are already added.
return result
}
}
}
}
}
if !l.host.DirectoryExists(baseDirectory) {
return result
}
// Enumerate all available files.
files := l.ReadDirectory(
baseDirectory,
extensionOptions.extensionsToSearch,
[]string{"./*"}, /*include*/
)
for _, filePath := range files {
if tspath.ComparePaths(exclude, filePath, tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: program.UseCaseSensitiveFileNames(),
CurrentDirectory: program.GetCurrentDirectory(),
}) == 0 {
continue // Avoid self-imports
}
name, extension := getFilenameWithExtensionOption(
tspath.GetBaseFileName(filePath),
program,
extensionOptions,
false, /*isExportsOrImportsWildcard*/
)
result.add(
moduleCompletionNameAndKind{
name: name,
kind: moduleCompletionKindFile,
extension: extension,
},
)
}
// Get folder completion as well.
directories := l.GetDirectories(baseDirectory)
for _, directory := range directories {
directoryName := tspath.GetBaseFileName(directory)
if directoryName != "@types" {
result.add(
moduleCompletionNameAndKind{
name: directoryName,
kind: moduleCompletionKindDirectory,
},
)
}
}
return result
}
// Returns true if `fragment` was a match for any `paths`
// (which should indicate whether any other path completions should be offered).
func (l *LanguageService) addCompletionEntriesFromPaths(
result *moduleCompletionNameAndKindSet,
program *compiler.Program,
fragment string,
baseDirectory string,
extensionOptions *extensionOptions,
paths *collections.OrderedMap[string, []string],
) bool {
getPatternsForKeys := func(key string) []string {
return paths.GetOrZero(key)
}
comparePaths := func(a, b string) stringutil.Comparison {
patternA := core.TryParsePattern(a)
patternB := core.TryParsePattern(b)
lengthA := len(a)
if patternA.StarIndex != -1 {
lengthA = patternA.StarIndex
}
lengthB := len(b)
if patternB.StarIndex != -1 {
lengthB = patternB.StarIndex
}
return cmp.Compare(lengthB, lengthA)
}
return l.addCompletionEntriesFromPathsOrExportsOrImports(
result,
program,
false, /*isExports*/
false, /*isImports*/
fragment,
baseDirectory,
extensionOptions,
paths.Keys(),
getPatternsForKeys,
comparePaths,
)
}
// Returns true if `fragment` was a match for any `paths`
// (which should indicate whether any other path completions should be offered).
func (l *LanguageService) addCompletionEntriesFromPathsOrExportsOrImports(
result *moduleCompletionNameAndKindSet,
program *compiler.Program,
isExports bool,
isImports bool,
fragment string,
baseDirectory string,
extensionOptions *extensionOptions,
keys iter.Seq[string],
getPatternsForKey func(key string) []string,
comparePaths func(a, b string) stringutil.Comparison,
) bool {
type pathResult struct {
results []moduleCompletionNameAndKind
matched bool
}
var pathResults []pathResult
var matchedPath *string
for key := range keys {
if key == "." {
continue
}
normalizedKey := strings.TrimPrefix(key, "./") // Remove leading "./"
if (isExports || isImports) && strings.HasSuffix(key, "/") { // Normalize trailing "/" to "/*"
normalizedKey = normalizedKey + "*"
}
patterns := getPatternsForKey(key)
if len(patterns) > 0 {
pathPattern := core.TryParsePattern(normalizedKey)
if !pathPattern.IsValid() {
continue
}
isMatch := pathPattern.Matches(fragment)
var isLongestMatch bool
if isMatch {
if matchedPath == nil {
isLongestMatch = true
} else {
isLongestMatch = comparePaths(normalizedKey, *matchedPath) == stringutil.ComparisonLessThan
}
}
if isLongestMatch {
// If this is a higher priority match than anything we've seen so far, previous results from matches are invalid, e.g.
// for `import {} from "some-package/|"` with a typesVersions:
// {
// "bar/*": ["bar/*"], // <-- 1. We add 'bar', but 'bar/*' doesn't match yet.
// "*": ["dist/*"], // <-- 2. We match here and add files from dist. 'bar' is still ok because it didn't come from a match.
// "foo/*": ["foo/*"] // <-- 3. We matched '*' earlier and added results from dist, but if 'foo/*' also matched,
// } results in dist would not be visible. 'bar' still stands because it didn't come from a match.
// This is especially important if `dist/foo` is a folder, because if we fail to clear results
// added by the '*' match, after typing `"some-package/foo/|"` we would get file results from both
// ./dist/foo and ./foo, when only the latter will actually be resolvable.
// See pathCompletionsTypesVersionsWildcard6.ts.
matchedPath = &normalizedKey
pathResults = core.Filter(pathResults, func(pr pathResult) bool {
return !pr.matched
})
}
if pathPattern.StarIndex == -1 ||
matchedPath == nil ||
comparePaths(normalizedKey, *matchedPath) != stringutil.ComparisonGreaterThan {
pathResults = append(pathResults, pathResult{
matched: isMatch,
results: l.getCompletionsForPathMapping(
normalizedKey,
patterns,
fragment,
baseDirectory,
isExports,
isImports,
extensionOptions,
program,
),
})
}
}
}
for _, pr := range pathResults {
for _, res := range pr.results {
result.add(res)
}
}
return matchedPath != nil
}
func (l *LanguageService) getCompletionsForPathMapping(
path string,
patterns []string,
fragment string,
packageDirectory string,
isExports bool,
isImports bool,
extensionOptions *extensionOptions,
program *compiler.Program,
) []moduleCompletionNameAndKind {
fragmentDirectory := getFragmentDirectory(fragment)
if fragmentDirectory != "" {
fragmentDirectory = tspath.EnsureTrailingDirectorySeparator(fragmentDirectory)
}
justPathMappingName := func(name string, kind moduleCompletionKind, extension string) []moduleCompletionNameAndKind {
if strings.HasPrefix(name, fragment) {
name = tspath.RemoveTrailingDirectorySeparator(name)
if fragmentDirectory != "" {
name = strings.TrimPrefix(name, fragmentDirectory)
}
return []moduleCompletionNameAndKind{{
name: name,
kind: kind,
extension: extension,
}}
}
return nil
}
parsedPath := core.TryParsePattern(path)
if !parsedPath.IsValid() {
return nil
}
// No stars in the pattern.
if parsedPath.StarIndex == -1 {
// For a path mapping "foo": ["/x/y/z.ts"], add "foo" itself as a completion.
pattern := core.FirstOrNil(patterns)
extension := getFileExtension(pattern)
return justPathMappingName(path, moduleCompletionKindFile, extension)
}
pathPrefix := parsedPath.Text[:parsedPath.StarIndex]
pathSuffix := parsedPath.Text[parsedPath.StarIndex+1:]
if !strings.HasPrefix(fragment, pathPrefix) {
// Fragment doesn't match the path mapping prefix at all:
// we cannot extend it via this path.
if !strings.HasPrefix(pathPrefix, fragment) {
return nil
}
starIsFullPathComponent := strings.HasSuffix(path, "/*")
if starIsFullPathComponent {
return justPathMappingName(pathPrefix, moduleCompletionKindDirectory, "" /*extension*/)
}
// If path is e.g. `foo/bar/*`, and fragment is `foo/b`, then remaining directory prefix is `bar/`,
remainingDirectoryPrefix := pathPrefix[len(fragmentDirectory):]
var completions []moduleCompletionNameAndKind
for _, pattern := range patterns {
modules := l.getModulesForPathsPattern(
"", /*fragment*/
packageDirectory,
pattern,
isExports,
isImports,
extensionOptions,
program,
)
for i := range modules {
modules[i].name = remainingDirectoryPrefix + modules[i].name + core.IfElse(modules[i].kind == moduleCompletionKindFile, pathSuffix, "")
}
completions = append(completions, modules...)
}
return completions
}
remainingFragment := fragment[len(pathPrefix):]
var remainingDirectoryFragment string
if !strings.HasPrefix(fragmentDirectory, pathPrefix) {
remainingDirectoryFragment = pathPrefix[len(fragmentDirectory):]
}
return core.FlatMap(
patterns,
func(pattern string) []moduleCompletionNameAndKind {
modules := l.getModulesForPathsPattern(
remainingFragment,
packageDirectory,
pattern,
isExports,
isImports,
extensionOptions,
program,
)
for i := range modules {
modules[i].name = remainingDirectoryFragment + modules[i].name + core.IfElse(modules[i].kind == moduleCompletionKindFile, pathSuffix, "")
}
return modules
},
)
}
func getFileExtension(fileName string) string {
extension := tspath.TryGetExtensionFromPath(fileName)
if extension == "" {
extension = tspath.GetAnyExtensionFromPath(fileName, nil /*extensions*/, false /*ignoreCase*/)
}
return extension
}
// The input fragment is relative to the path pattern's prefix:
// e.g. if path = "bar/_*/baz", and fragment = "bar/_dir", then fragment is "dir".
// The names are relative to the path pattern's prefix and fragment directory :
// e.g. if path = "bar/_*/baz", and fragment = "bar/_dir/a", and we find result "abd",
// the result should be interpreted as "bar/_dir/abd".
func (l *LanguageService) getModulesForPathsPattern(
fragment string,
packageDirectory string,
pattern string,
isExports bool,
isImports bool,
extensionOptions *extensionOptions,
program *compiler.Program,
) []moduleCompletionNameAndKind {
parsed := core.TryParsePattern(pattern)
if !parsed.IsValid() || parsed.StarIndex == -1 {
return nil
}
prefix := parsed.Text[:parsed.StarIndex]
suffix := parsed.Text[parsed.StarIndex+1:]
// The prefix has two effective parts: the directory path and the base component after the filepath that is not a
// full directory component. For example: directory/path/of/prefix/base*
normalizedPrefix := tspath.ResolvePath(prefix)
var normalizedPrefixDirectory string
var normalizedPrefixBase string
if tspath.HasTrailingDirectorySeparator(prefix) {
normalizedPrefixDirectory = normalizedPrefix
normalizedPrefixBase = ""
} else {
normalizedPrefixDirectory = tspath.GetDirectoryPath(normalizedPrefix)
normalizedPrefixBase = tspath.GetBaseFileName(normalizedPrefix)
}
fragmentHasPath := containsSlash(fragment)
var fragmentDirectory string
if fragmentHasPath {
if tspath.HasTrailingDirectorySeparator(fragment) {
fragmentDirectory = fragment
} else {
fragmentDirectory = tspath.GetDirectoryPath(fragment)
}
}
options := program.Options()
ignoreCase := !program.UseCaseSensitiveFileNames()
outDir := options.OutDir
declarationDir := options.DeclarationDir
// Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
var expandedPrefixDirectory string
if fragmentHasPath {
expandedPrefixDirectory = tspath.CombinePaths(normalizedPrefixDirectory, normalizedPrefixBase+fragmentDirectory)
} else {
expandedPrefixDirectory = normalizedPrefixDirectory
}
// Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b".
baseDirectory := tspath.NormalizePath(tspath.CombinePaths(packageDirectory, expandedPrefixDirectory))
var possibleInputBaseDirectoryForOutDir string
var possibleInputBaseDirectoryForDeclarationDir string
if isImports {
if outDir != "" {
possibleInputBaseDirectoryForOutDir = getPossibleOriginalInputPathWithoutChangingExt(
baseDirectory,
ignoreCase,
outDir,
program.CommonSourceDirectory,
)
}
if declarationDir != "" {
possibleInputBaseDirectoryForDeclarationDir = getPossibleOriginalInputPathWithoutChangingExt(
baseDirectory,
ignoreCase,
declarationDir,
program.CommonSourceDirectory,
)
}
}
normalizedSuffix := tspath.NormalizePath(suffix)
var declarationExtension string
var inputExtensions []string
if normalizedSuffix != "" {
declarationExtension = tspath.GetDeclarationEmitExtensionForPath("_" + normalizedSuffix)
inputExtensions = tspath.GetPossibleOriginalInputExtensionForExtension("_" + normalizedSuffix)
}
var matchingSuffixes []string
if declarationExtension != "" {
matchingSuffixes = append(matchingSuffixes, tspath.ChangeExtension(normalizedSuffix, declarationExtension))
}
for _, ext := range inputExtensions {
matchingSuffixes = append(matchingSuffixes, tspath.ChangeExtension(normalizedSuffix, ext))
}
matchingSuffixes = append(matchingSuffixes, normalizedSuffix)
// If we have a suffix, then we read the directory all the way down to avoid returning completions for
// directories that don't contain files that would match the suffix. A previous comment here was concerned
// about the case where `normalizedSuffix` includes a `?` character, which should be interpreted literally,
// but will match any single character as part of the `include` pattern in `tryReadDirectory`. This is not
// a problem, because (in the extremely unusual circumstance where the suffix has a `?` in it) a `?`
// interpreted as "any character" can only return *too many* results as compared to the literal
// interpretation, so we can filter those superfluous results out via `trimPrefixAndSuffix` as we've always
// done.
var includeGlobs []string
if normalizedSuffix != "" {
for _, suffix := range matchingSuffixes {
includeGlobs = append(includeGlobs, "**/*"+suffix)
}
} else {
includeGlobs = []string{"./*"}
}
isExportsOrImportsWildcard := (isExports || isImports) && strings.HasSuffix(pattern, "/*")
trimPrefixAndSuffix := func(path string, prefixStr string) string {
for _, suffix := range matchingSuffixes {
inner := withoutStartAndEnd(tspath.NormalizePath(path), prefixStr, suffix)
if inner == nil {
continue
}
return removeLeadingDirectorySeparator(*inner)
}
return ""
}
getMatchesWithPrefix := func(directory string) []moduleCompletionNameAndKind {
var completePrefix string
if fragmentHasPath {
completePrefix = directory
} else {
completePrefix = tspath.EnsureTrailingDirectorySeparator(directory) + normalizedPrefixBase
}
matches := l.ReadDirectory(
directory,
extensionOptions.extensionsToSearch,
includeGlobs,
)
var result []moduleCompletionNameAndKind
for _, match := range matches {
trimmedWithPattern := trimPrefixAndSuffix(match, completePrefix)
if trimmedWithPattern != "" {
if containsSlash(trimmedWithPattern) {
pathComponents := tspath.GetPathComponents(removeLeadingDirectorySeparator(trimmedWithPattern), "")
if len(pathComponents) > 1 {
result = append(result, moduleCompletionNameAndKind{
name: pathComponents[1],
kind: moduleCompletionKindDirectory,
})
}
} else {
name, extension := getFilenameWithExtensionOption(
trimmedWithPattern,
program,
extensionOptions,
isExportsOrImportsWildcard,
)
if extension == "" {
extension = getFileExtension(match)
}
result = append(result, moduleCompletionNameAndKind{
name: name,
kind: moduleCompletionKindFile,
extension: extension,
})
}
}
}
return result
}
getDirectoryMatches := func(directoryName string) []moduleCompletionNameAndKind {
directories := l.GetDirectories(directoryName)
var result []moduleCompletionNameAndKind
for _, dir := range directories {
if dir != "node_modules" {
result = append(result, moduleCompletionNameAndKind{
name: dir,
kind: moduleCompletionKindDirectory,
})
}
}
return result
}
var matches []moduleCompletionNameAndKind
matches = append(matches, getMatchesWithPrefix(baseDirectory)...)
if possibleInputBaseDirectoryForOutDir != "" {
matches = append(matches, getMatchesWithPrefix(possibleInputBaseDirectoryForOutDir)...)
}
if possibleInputBaseDirectoryForDeclarationDir != "" {
matches = append(matches, getMatchesWithPrefix(possibleInputBaseDirectoryForDeclarationDir)...)
}
// If we had a suffix, we already recursively searched for all possible files that could match
// it and returned the directories leading to those files. Otherwise, assume any directory could
// have something valid to import.
if normalizedSuffix == "" {
matches = append(matches, getDirectoryMatches(baseDirectory)...)
if possibleInputBaseDirectoryForOutDir != "" {
matches = append(matches, getDirectoryMatches(possibleInputBaseDirectoryForOutDir)...)
}
if possibleInputBaseDirectoryForDeclarationDir != "" {
matches = append(matches, getDirectoryMatches(possibleInputBaseDirectoryForDeclarationDir)...)
}
}
return matches
}
func containsSlash(fragment string) bool {
return strings.Contains(fragment, string(tspath.DirectorySeparator))
}
func withoutStartAndEnd(s string, start string, end string) *string {
if strings.HasPrefix(s, start) && strings.HasSuffix(s, end) && len(s) >= len(start)+len(end) {
s = s[len(start) : len(s)-len(end)]
return &s
}
return nil
}
func removeLeadingDirectorySeparator(path string) string {
return strings.TrimPrefix(path, string(tspath.DirectorySeparator))
}
func getPossibleOriginalInputPathWithoutChangingExt(
filePath string,
ignoreCase bool,
outputDir string,
getCommonSourceDirectory func() string,
) string {
if outputDir != "" {
return tspath.ResolvePath(
getCommonSourceDirectory(),
tspath.GetRelativePathFromDirectory(outputDir, filePath, tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: !ignoreCase,
}),
)
}
return filePath
}
func getFilenameWithExtensionOption(
name string,
program *compiler.Program,
extensionOptions *extensionOptions,
isExportsOrImportsWildcard bool,
) (string, string) {
nonJSResult := modulespecifiers.TryGetRealFileNameForNonJSDeclarationFileName(name)
if nonJSResult != "" {
return nonJSResult, tspath.TryGetExtensionFromPath(nonJSResult)
}
if extensionOptions.referenceKind == referenceKindFileName {
return name, tspath.TryGetExtensionFromPath(name)
}
allowedEndings := modulespecifiers.GetAllowedEndingsInPreferredOrder(
modulespecifiers.UserPreferences{ImportModuleSpecifierEnding: extensionOptions.endingPreference},
program,
program.Options(),
extensionOptions.importingSourceFile,
"", /*oldImportSpecifier*/
extensionOptions.resolutionMode,
)
if isExportsOrImportsWildcard {
// If we're completing `import {} from "foo/|"` and subpaths are available via `"exports": { "./*": "./src/*" }`,
// the completion must be a (potentially extension-swapped) file name. Dropping extensions and index files is not allowed.
allowedEndings = core.Filter(allowedEndings, func(e modulespecifiers.ModuleSpecifierEnding) bool {
return e != modulespecifiers.ModuleSpecifierEndingMinimal && e != modulespecifiers.ModuleSpecifierEndingIndex
})
}
if len(allowedEndings) > 0 && allowedEndings[0] == modulespecifiers.ModuleSpecifierEndingTsExtension {
if tspath.FileExtensionIsOneOf(name, tspath.SupportedTSImplementationExtensions) {
return name, tspath.TryGetExtensionFromPath(name)
}
outputExtension := module.TryGetJSExtensionForFile(name, program.Options())
if outputExtension != "" {
return tspath.ChangeExtension(name, outputExtension), outputExtension
}
return name, tspath.TryGetExtensionFromPath(name)
}
if !isExportsOrImportsWildcard &&
len(allowedEndings) > 0 &&
(allowedEndings[0] == modulespecifiers.ModuleSpecifierEndingMinimal || allowedEndings[0] == modulespecifiers.ModuleSpecifierEndingIndex) &&
tspath.FileExtensionIsOneOf(name, []string{tspath.ExtensionJs, tspath.ExtensionJsx, tspath.ExtensionTs, tspath.ExtensionTsx, tspath.ExtensionDts}) {
return tspath.RemoveFileExtension(name), tspath.TryGetExtensionFromPath(name)
}
outputExtension := module.TryGetJSExtensionForFile(name, program.Options())
if outputExtension != "" {
return tspath.ChangeExtension(name, outputExtension), outputExtension
}
return name, tspath.TryGetExtensionFromPath(name)
}
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.UnionTypeNodeNode, current *ast.LiteralTypeNodeNode) []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) lsutil.ScriptElementKindModifier {
switch extension {
case tspath.ExtensionDts:
return lsutil.ScriptElementKindModifierDts
case tspath.ExtensionJs:
return lsutil.ScriptElementKindModifierJs
case tspath.ExtensionJson:
return lsutil.ScriptElementKindModifierJson
case tspath.ExtensionJsx:
return lsutil.ScriptElementKindModifierJsx
case tspath.ExtensionTs:
return lsutil.ScriptElementKindModifierTs
case tspath.ExtensionTsx:
return lsutil.ScriptElementKindModifierTsx
case tspath.ExtensionDmts:
return lsutil.ScriptElementKindModifierDmts
case tspath.ExtensionMjs:
return lsutil.ScriptElementKindModifierMjs
case tspath.ExtensionMts:
return lsutil.ScriptElementKindModifierMts
case tspath.ExtensionDcts:
return lsutil.ScriptElementKindModifierDcts
case tspath.ExtensionCjs:
return lsutil.ScriptElementKindModifierCjs
case tspath.ExtensionCts:
return lsutil.ScriptElementKindModifierCts
case tspath.ExtensionTsBuildInfo:
panic(fmt.Sprintf("Extension %v is unsupported.", tspath.ExtensionTsBuildInfo))
default:
return lsutil.ScriptElementKindModifierNone
}
}
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,
docFormat lsproto.MarkupKind,
) *lsproto.CompletionItem {
if contextToken == nil || !ast.IsStringLiteralLike(contextToken) {
return item
}
completions := l.getStringLiteralCompletionEntries(
ctx,
file,
contextToken,
position,
checker,
)
if completions == nil {
return item
}
return l.stringLiteralCompletionDetails(item, name, contextToken, position, completions, file, checker, docFormat)
}
func (l *LanguageService) stringLiteralCompletionDetails(
item *lsproto.CompletionItem,
name string,
location *ast.Node,
position int,
completion *stringLiteralCompletions,
file *ast.SourceFile,
checker *checker.Checker,
docFormat lsproto.MarkupKind,
) *lsproto.CompletionItem {
switch {
case completion.fromPaths != nil:
// Path completions have eagerly-resolved details so the client can show an accurate icon
// for items of file kind based on the file extension provided in the item detail.
return item
case completion.fromProperties != nil:
properties := completion.fromProperties
for _, symbol := range properties.symbols {
if symbol.Name == name {
return l.createCompletionDetailsForSymbol(item, symbol, checker, location, position, docFormat)
}
}
case completion.fromTypes != nil:
types := completion.fromTypes
for _, t := range types.types {
if t.AsLiteralType().Value().(string) == name {
return createCompletionDetails(item, name, "" /*documentation*/, docFormat)
}
}
}
return item
}
func isInReferenceComment(file *ast.SourceFile, position int) bool {
commentRange := isInComment(file, position, astnav.GetTokenAtPosition(file, position))
if commentRange == nil {
return false
}
commentText := file.Text()[commentRange.Pos():commentRange.End()]
return hasTripleSlashPrefix(commentText)
}
func hasTripleSlashPrefix(commentText string) bool {
return strings.HasPrefix(commentText, "///") && strings.HasPrefix(strings.TrimSpace(commentText[3:]), "<")
}
// Matches a triple slash reference directive with an incomplete string literal for its path.
// Used to determine if the caret is currently within the string literal and capture the literal
// fragment for completions.
// For example, this matches
//
// /// <reference path="fragment
//
// but not
//
// /// <reference path="fragment"
// Returns (prefix, kind, toComplete, ok) where:
// - prefix is everything up to and including the opening quote
// - kind is either "path" or "types"
// - toComplete is the fragment after the opening quote
// - ok indicates whether the match was successful
func parseTripleSlashDirectiveFragment(text string) (prefix string, kind string, toComplete string, ok bool) {
rest := text
if !strings.HasPrefix(rest, "///") {
return "", "", "", false
}
rest = rest[len("///"):]
rest = strings.TrimLeftFunc(rest, stringutil.IsWhiteSpaceLike)
// <reference
if !strings.HasPrefix(rest, "<reference") {
return "", "", "", false
}
rest = rest[len("<reference"):]
if len(rest) == 0 || !stringutil.IsWhiteSpaceLike(rune(rest[0])) {
return "", "", "", false
}
rest = strings.TrimLeftFunc(rest, stringutil.IsWhiteSpaceLike)
// path or types
if strings.HasPrefix(rest, "path") {
kind = "path"
rest = rest[len("path"):]
} else if strings.HasPrefix(rest, "types") {
kind = "types"
rest = rest[len("types"):]
} else {
return "", "", "", false
}
// Skip optional whitespace, then must have "="
rest = strings.TrimLeftFunc(rest, stringutil.IsWhiteSpaceLike)
if !strings.HasPrefix(rest, "=") {
return "", "", "", false
}
rest = rest[1:]
// Skip optional whitespace, then must have opening quote (' or ")
rest = strings.TrimLeftFunc(rest, stringutil.IsWhiteSpaceLike)
if len(rest) == 0 || (rest[0] != '\'' && rest[0] != '"') {
return "", "", "", false
}
rest = rest[1:]
// The toComplete part is everything after the opening quote
if strings.ContainsAny(rest, `'"`) {
return "", "", "", false
}
toComplete = rest
prefix = text[:len(text)-len(toComplete)]
return prefix, kind, toComplete, true
}
func (l *LanguageService) getTripleSlashReferenceCompletions(
file *ast.SourceFile,
position int,
program *compiler.Program,
checker *checker.Checker,
) []*pathCompletion {
compilerOptions := program.Options()
token := astnav.GetTokenAtPosition(file, position)
commentRanges := slices.Collect(scanner.GetLeadingCommentRanges(&ast.NodeFactory{}, file.Text(), token.Pos()))
var foundRange *ast.CommentRange
for i := range commentRanges {
commentRange := &commentRanges[i]
if position >= commentRange.Pos() && position <= commentRange.End() {
foundRange = commentRange
break
}
}
if foundRange == nil {
return nil
}
text := file.Text()[foundRange.Pos():position]
prefix, kind, toComplete, ok := parseTripleSlashDirectiveFragment(text)
if !ok {
return nil
}
scriptPath := tspath.GetDirectoryPath(string(file.Path()))
var names []moduleCompletionNameAndKind
switch kind {
case "path":
extensionOptions := l.getExtensionOptions(compilerOptions, referenceKindFileName, file, core.ResolutionModeNone, nil /*checker*/)
result := l.getCompletionEntriesForDirectoryFragment(
toComplete,
scriptPath,
extensionOptions,
program,
true, /*moduleSpecifierIsRelative*/
string(file.Path()),
&moduleCompletionNameAndKindSet{names: make(map[string]moduleCompletionNameAndKind)},
)
names = slices.Collect(maps.Values(result.names))
case "types":
extensionOptions := l.getExtensionOptions(compilerOptions, referenceKindModuleSpecifier, file, core.ResolutionModeNone, nil /*checker*/)
result := &moduleCompletionNameAndKindSet{names: make(map[string]moduleCompletionNameAndKind)}
l.getCompletionEntriesFromTypings(program, scriptPath, getFragmentDirectory(toComplete), extensionOptions, result)
names = slices.Collect(maps.Values(result.names))
}
return addReplacementSpans(toComplete, foundRange.Pos()+len(prefix), names)
}