257 lines
8.5 KiB
Go
257 lines
8.5 KiB
Go
package autoimport
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/microsoft/typescript-go/internal/ast"
|
|
"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/scanner"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
)
|
|
|
|
type View struct {
|
|
registry *Registry
|
|
importingFile *ast.SourceFile
|
|
program *compiler.Program
|
|
preferences modulespecifiers.UserPreferences
|
|
projectKey tspath.Path
|
|
|
|
allowedEndings []modulespecifiers.ModuleSpecifierEnding
|
|
conditions *collections.Set[string]
|
|
shouldUseUriStyleNodeCoreModules core.Tristate
|
|
existingImports *collections.MultiMap[ModuleID, existingImport]
|
|
shouldUseRequireForFixes *bool
|
|
}
|
|
|
|
func NewView(registry *Registry, importingFile *ast.SourceFile, projectKey tspath.Path, program *compiler.Program, preferences modulespecifiers.UserPreferences) *View {
|
|
return &View{
|
|
registry: registry,
|
|
importingFile: importingFile,
|
|
program: program,
|
|
projectKey: projectKey,
|
|
preferences: preferences,
|
|
conditions: collections.NewSetFromItems(
|
|
module.GetConditions(program.Options(),
|
|
program.GetDefaultResolutionModeForFile(importingFile))...,
|
|
),
|
|
shouldUseUriStyleNodeCoreModules: lsutil.ShouldUseUriStyleNodeCoreModules(importingFile, program),
|
|
}
|
|
}
|
|
|
|
func (v *View) getAllowedEndings() []modulespecifiers.ModuleSpecifierEnding {
|
|
if v.allowedEndings == nil {
|
|
resolutionMode := v.program.GetDefaultResolutionModeForFile(v.importingFile)
|
|
v.allowedEndings = modulespecifiers.GetAllowedEndingsInPreferredOrder(
|
|
v.preferences,
|
|
v.program,
|
|
v.program.Options(),
|
|
v.importingFile,
|
|
"",
|
|
resolutionMode,
|
|
)
|
|
}
|
|
return v.allowedEndings
|
|
}
|
|
|
|
type QueryKind int
|
|
|
|
const (
|
|
QueryKindWordPrefix QueryKind = iota
|
|
QueryKindExactMatch
|
|
QueryKindCaseInsensitiveMatch
|
|
)
|
|
|
|
func (v *View) Search(query string, kind QueryKind) []*Export {
|
|
searchFn := func(bucket *RegistryBucket) []*Export {
|
|
switch kind {
|
|
case QueryKindWordPrefix:
|
|
return bucket.Index.SearchWordPrefix(query)
|
|
case QueryKindExactMatch:
|
|
return bucket.Index.Find(query, true)
|
|
case QueryKindCaseInsensitiveMatch:
|
|
return bucket.Index.Find(query, false)
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
}
|
|
|
|
return v.search(searchFn)
|
|
}
|
|
|
|
func (v *View) SearchByExportID(id ExportID) []*Export {
|
|
search := func(bucket *RegistryBucket) []*Export {
|
|
return core.Filter(bucket.Index.entries, func(e *Export) bool {
|
|
return e.ExportID == id
|
|
})
|
|
}
|
|
|
|
return v.search(search)
|
|
}
|
|
|
|
func (v *View) search(searchFn func(*RegistryBucket) []*Export) []*Export {
|
|
var results []*Export
|
|
|
|
if bucket, ok := v.registry.projects[v.projectKey]; ok {
|
|
exports := searchFn(bucket)
|
|
results = slices.Grow(results, len(exports))
|
|
for _, e := range exports {
|
|
if string(e.ModuleID) == string(v.importingFile.Path()) {
|
|
// Don't auto-import from the importing file itself
|
|
continue
|
|
}
|
|
results = append(results, e)
|
|
}
|
|
}
|
|
|
|
// Compute the set of packages accessible to the importing file.
|
|
// This includes packages from package.json dependencies (aggregated from ancestor directories)
|
|
// plus packages that are directly imported by the project's program files.
|
|
// If no package.json is found, allowedPackages remains nil and all packages are allowed.
|
|
var allowedPackages *collections.Set[string]
|
|
tspath.ForEachAncestorDirectoryPath(v.importingFile.Path().GetDirectoryPath(), func(dirPath tspath.Path) (result any, stop bool) {
|
|
if dir, ok := v.registry.directories[dirPath]; ok {
|
|
if pj := dir.packageJson; pj.Exists() && pj.Contents.Parseable {
|
|
// Initialize to empty set if this is the first package.json we've seen
|
|
if allowedPackages == nil {
|
|
allowedPackages = &collections.Set[string]{}
|
|
}
|
|
addPackageJsonDependencies(pj.Contents, allowedPackages)
|
|
}
|
|
}
|
|
return nil, false
|
|
})
|
|
// If we found at least one package.json, also include packages directly imported by the project
|
|
if allowedPackages != nil {
|
|
if bucket, ok := v.registry.projects[v.projectKey]; ok {
|
|
allowedPackages = allowedPackages.UnionedWith(bucket.ResolvedPackageNames)
|
|
}
|
|
}
|
|
|
|
excludePackages := &collections.Set[string]{}
|
|
tspath.ForEachAncestorDirectoryPath(v.importingFile.Path().GetDirectoryPath(), func(dirPath tspath.Path) (result any, stop bool) {
|
|
if nodeModulesBucket, ok := v.registry.nodeModules[dirPath]; ok {
|
|
exports := searchFn(nodeModulesBucket)
|
|
results = slices.Grow(results, len(exports))
|
|
for _, e := range exports {
|
|
// Exclude packages found in lower node_modules (shadowing)
|
|
if excludePackages.Has(e.PackageName) {
|
|
continue
|
|
}
|
|
// If allowedPackages is nil, no package.json was found, so include all packages.
|
|
// Otherwise, only include packages that are dependencies or directly imported.
|
|
if allowedPackages != nil && !allowedPackages.Has(e.PackageName) {
|
|
continue
|
|
}
|
|
results = append(results, e)
|
|
}
|
|
|
|
// As we go up the directory tree, exclude packages found in lower node_modules
|
|
for pkgName := range nodeModulesBucket.PackageFiles {
|
|
excludePackages.Add(pkgName)
|
|
}
|
|
}
|
|
return nil, false
|
|
})
|
|
return results
|
|
}
|
|
|
|
type FixAndExport struct {
|
|
Fix *Fix
|
|
Export *Export
|
|
}
|
|
|
|
func (v *View) GetCompletions(ctx context.Context, prefix string, position lsproto.Position, forJSX bool, isTypeOnlyLocation bool) []*FixAndExport {
|
|
results := v.Search(prefix, QueryKindWordPrefix)
|
|
|
|
type exportGroupKey struct {
|
|
target ExportID
|
|
name string
|
|
ambientModuleOrPackageName string
|
|
}
|
|
grouped := make(map[exportGroupKey][]*Export, len(results))
|
|
outer:
|
|
for _, e := range results {
|
|
name := e.Name()
|
|
if !scanner.IsIdentifierText(name, core.LanguageVariantStandard) {
|
|
continue
|
|
}
|
|
if forJSX && !(unicode.IsUpper(rune(name[0])) || e.IsRenameable()) {
|
|
continue
|
|
}
|
|
target := e.ExportID
|
|
if e.Target != (ExportID{}) {
|
|
target = e.Target
|
|
}
|
|
key := exportGroupKey{
|
|
target: target,
|
|
name: name,
|
|
ambientModuleOrPackageName: core.FirstNonZero(e.AmbientModuleName(), e.PackageName),
|
|
}
|
|
if e.PackageName == "@types/node" || strings.Contains(string(e.Path), "/node_modules/@types/node/") {
|
|
if _, ok := core.UnprefixedNodeCoreModules[key.ambientModuleOrPackageName]; ok {
|
|
// Group URI-style and non-URI style node core modules together so the ranking logic
|
|
// is allowed to drop one if an explicit preference is detected.
|
|
key.ambientModuleOrPackageName = "node:" + key.ambientModuleOrPackageName
|
|
}
|
|
}
|
|
if existing, ok := grouped[key]; ok {
|
|
for i, ex := range existing {
|
|
if e.ExportID == ex.ExportID {
|
|
grouped[key] = slices.Replace(existing, i, i+1, &Export{
|
|
ExportID: e.ExportID,
|
|
ModuleFileName: e.ModuleFileName,
|
|
PackageName: e.PackageName,
|
|
IsTypeOnly: e.IsTypeOnly || ex.IsTypeOnly,
|
|
Syntax: min(e.Syntax, ex.Syntax),
|
|
Flags: e.Flags | ex.Flags,
|
|
ScriptElementKind: min(e.ScriptElementKind, ex.ScriptElementKind),
|
|
ScriptElementKindModifiers: e.ScriptElementKindModifiers | ex.ScriptElementKindModifiers,
|
|
localName: e.localName,
|
|
Target: e.Target,
|
|
Path: e.Path,
|
|
})
|
|
continue outer
|
|
}
|
|
}
|
|
}
|
|
grouped[key] = append(grouped[key], e)
|
|
}
|
|
|
|
fixes := make([]*FixAndExport, 0, len(results))
|
|
compareFixes := func(a, b *FixAndExport) int {
|
|
return v.CompareFixesForRanking(a.Fix, b.Fix)
|
|
}
|
|
|
|
for _, exps := range grouped {
|
|
fixesForGroup := make([]*FixAndExport, 0, len(exps))
|
|
for _, e := range exps {
|
|
for _, fix := range v.GetFixes(ctx, e, forJSX, isTypeOnlyLocation, &position) {
|
|
fixesForGroup = append(fixesForGroup, &FixAndExport{
|
|
Fix: fix,
|
|
Export: e,
|
|
})
|
|
}
|
|
}
|
|
fixes = append(fixes, core.MinAllFunc(fixesForGroup, compareFixes)...)
|
|
}
|
|
|
|
// The client will do additional sorting by SortText and Label, so we don't
|
|
// need to consider the name in our sorting here; we only need to produce a
|
|
// stable relative ordering between completions that the client will consider
|
|
// equivalent.
|
|
slices.SortFunc(fixes, func(a, b *FixAndExport) int {
|
|
return v.CompareFixesForSorting(a.Fix, b.Fix)
|
|
})
|
|
|
|
return fixes
|
|
}
|