2212 lines
71 KiB
Go
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)
|
|
}
|