vendor tsgo
This commit is contained in:
44
tools/tsgo/internal/ls/api.go
Normal file
44
tools/tsgo/internal/ls/api.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoSourceFile = errors.New("source file not found")
|
||||
ErrNoTokenAtPosition = errors.New("no token found at position")
|
||||
)
|
||||
|
||||
func (l *LanguageService) GetSymbolAtPosition(ctx context.Context, fileName string, position int) (*ast.Symbol, error) {
|
||||
program, file := l.tryGetProgramAndFile(fileName)
|
||||
if file == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNoSourceFile, fileName)
|
||||
}
|
||||
node := astnav.GetTokenAtPosition(file, position)
|
||||
if node == nil {
|
||||
return nil, fmt.Errorf("%w: %s:%d", ErrNoTokenAtPosition, fileName, position)
|
||||
}
|
||||
checker, done := program.GetTypeCheckerForFile(ctx, file)
|
||||
defer done()
|
||||
return checker.GetSymbolAtLocation(node), nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) GetSymbolAtLocation(ctx context.Context, node *ast.Node) *ast.Symbol {
|
||||
program := l.GetProgram()
|
||||
checker, done := program.GetTypeCheckerForFile(ctx, ast.GetSourceFileOfNode(node))
|
||||
defer done()
|
||||
return checker.GetSymbolAtLocation(node)
|
||||
}
|
||||
|
||||
func (l *LanguageService) GetTypeOfSymbol(ctx context.Context, symbol *ast.Symbol) *checker.Type {
|
||||
program := l.GetProgram()
|
||||
checker, done := program.GetTypeChecker(ctx)
|
||||
defer done()
|
||||
return checker.GetTypeOfSymbolAtLocation(symbol, nil)
|
||||
}
|
||||
236
tools/tsgo/internal/ls/autoimport/aliasresolver.go
Normal file
236
tools/tsgo/internal/ls/autoimport/aliasresolver.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/binder"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/module"
|
||||
"github.com/microsoft/typescript-go/internal/packagejson"
|
||||
"github.com/microsoft/typescript-go/internal/symlinks"
|
||||
"github.com/microsoft/typescript-go/internal/tsoptions"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
type pathAndFileName struct {
|
||||
path tspath.Path
|
||||
fileName string
|
||||
}
|
||||
|
||||
type aliasResolver struct {
|
||||
toPath func(fileName string) tspath.Path
|
||||
host RegistryCloneHost
|
||||
moduleResolver *module.Resolver
|
||||
|
||||
rootFiles []*ast.SourceFile
|
||||
// symlinks maps from realpath to symlinked path and file name
|
||||
symlinks map[tspath.Path]pathAndFileName
|
||||
onFailedAmbientModuleLookup func(source ast.HasFileName, moduleName string)
|
||||
resolvedModules collections.SyncMap[tspath.Path, *collections.SyncMap[module.ModeAwareCacheKey, *module.ResolvedModule]]
|
||||
}
|
||||
|
||||
func newAliasResolver(
|
||||
rootFiles []*ast.SourceFile,
|
||||
symlinks map[tspath.Path]pathAndFileName,
|
||||
host RegistryCloneHost,
|
||||
moduleResolver *module.Resolver,
|
||||
toPath func(fileName string) tspath.Path,
|
||||
onFailedAmbientModuleLookup func(source ast.HasFileName, moduleName string),
|
||||
) *aliasResolver {
|
||||
r := &aliasResolver{
|
||||
toPath: toPath,
|
||||
host: host,
|
||||
moduleResolver: moduleResolver,
|
||||
rootFiles: rootFiles,
|
||||
symlinks: symlinks,
|
||||
onFailedAmbientModuleLookup: onFailedAmbientModuleLookup,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// BindSourceFiles implements checker.Program.
|
||||
func (r *aliasResolver) BindSourceFiles() {
|
||||
// We will bind as we parse
|
||||
}
|
||||
|
||||
// SourceFiles implements checker.Program.
|
||||
func (r *aliasResolver) SourceFiles() []*ast.SourceFile {
|
||||
return r.rootFiles
|
||||
}
|
||||
|
||||
// Options implements checker.Program.
|
||||
func (r *aliasResolver) Options() *core.CompilerOptions {
|
||||
return &core.CompilerOptions{
|
||||
NoCheck: core.TSTrue,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentDirectory implements checker.Program.
|
||||
func (r *aliasResolver) GetCurrentDirectory() string {
|
||||
return r.host.GetCurrentDirectory()
|
||||
}
|
||||
|
||||
// UseCaseSensitiveFileNames implements checker.Program.
|
||||
func (r *aliasResolver) UseCaseSensitiveFileNames() bool {
|
||||
return r.host.FS().UseCaseSensitiveFileNames()
|
||||
}
|
||||
|
||||
// GetSourceFile implements checker.Program.
|
||||
func (r *aliasResolver) GetSourceFile(fileName string) *ast.SourceFile {
|
||||
file := r.host.GetSourceFile(fileName, r.toPath(fileName))
|
||||
// file may be nil due to symlink/realpath mismatch; see TestAutoImportBuilderFS
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
binder.BindSourceFile(file)
|
||||
return file
|
||||
}
|
||||
|
||||
// GetDefaultResolutionModeForFile implements checker.Program.
|
||||
func (r *aliasResolver) GetDefaultResolutionModeForFile(file ast.HasFileName) core.ResolutionMode {
|
||||
return core.ModuleKindESNext
|
||||
}
|
||||
|
||||
// GetEmitModuleFormatOfFile implements checker.Program.
|
||||
func (r *aliasResolver) GetEmitModuleFormatOfFile(sourceFile ast.HasFileName) core.ModuleKind {
|
||||
return core.ModuleKindESNext
|
||||
}
|
||||
|
||||
// GetEmitSyntaxForUsageLocation implements checker.Program.
|
||||
func (r *aliasResolver) GetEmitSyntaxForUsageLocation(sourceFile ast.HasFileName, usageLocation *ast.StringLiteralLike) core.ResolutionMode {
|
||||
return core.ModuleKindESNext
|
||||
}
|
||||
|
||||
// GetImpliedNodeFormatForEmit implements checker.Program.
|
||||
func (r *aliasResolver) GetImpliedNodeFormatForEmit(sourceFile ast.HasFileName) core.ModuleKind {
|
||||
return core.ModuleKindESNext
|
||||
}
|
||||
|
||||
// GetModeForUsageLocation implements checker.Program.
|
||||
func (r *aliasResolver) GetModeForUsageLocation(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) core.ResolutionMode {
|
||||
return core.ModuleKindESNext
|
||||
}
|
||||
|
||||
// GetResolvedModule implements checker.Program.
|
||||
func (r *aliasResolver) GetResolvedModule(currentSourceFile ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule {
|
||||
cache, _ := r.resolvedModules.LoadOrStore(currentSourceFile.Path(), &collections.SyncMap[module.ModeAwareCacheKey, *module.ResolvedModule]{})
|
||||
if resolved, ok := cache.Load(module.ModeAwareCacheKey{Name: moduleReference, Mode: mode}); ok {
|
||||
return resolved
|
||||
}
|
||||
resolved, _ := r.moduleResolver.ResolveModuleName(moduleReference, currentSourceFile.FileName(), mode, nil)
|
||||
resolved, _ = cache.LoadOrStore(module.ModeAwareCacheKey{Name: moduleReference, Mode: mode}, resolved)
|
||||
if !resolved.IsResolved() && !tspath.PathIsRelative(moduleReference) {
|
||||
r.onFailedAmbientModuleLookup(currentSourceFile, moduleReference)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
// GetSourceFileForResolvedModule implements checker.Program.
|
||||
func (r *aliasResolver) GetSourceFileForResolvedModule(fileName string) *ast.SourceFile {
|
||||
return r.GetSourceFile(fileName)
|
||||
}
|
||||
|
||||
// GetResolvedModules implements checker.Program.
|
||||
func (r *aliasResolver) GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule] {
|
||||
// only used when producing diagnostics, which hopefully the checker won't do
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
// GetSymlinkCache implements checker.Program.
|
||||
func (r *aliasResolver) GetSymlinkCache() *symlinks.KnownSymlinks {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetSourceFileMetaData implements checker.Program.
|
||||
func (r *aliasResolver) GetSourceFileMetaData(path tspath.Path) ast.SourceFileMetaData {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// CommonSourceDirectory implements checker.Program.
|
||||
func (r *aliasResolver) CommonSourceDirectory() string {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// FileExists implements checker.Program.
|
||||
func (r *aliasResolver) FileExists(fileName string) bool {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetGlobalTypingsCacheLocation implements checker.Program.
|
||||
func (r *aliasResolver) GetGlobalTypingsCacheLocation() string {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetImportHelpersImportSpecifier implements checker.Program.
|
||||
func (r *aliasResolver) GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetJSXRuntimeImportSpecifier implements checker.Program.
|
||||
func (r *aliasResolver) GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetNearestAncestorDirectoryWithPackageJson implements checker.Program.
|
||||
func (r *aliasResolver) GetNearestAncestorDirectoryWithPackageJson(dirname string) string {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetPackageJsonInfo implements checker.Program.
|
||||
func (r *aliasResolver) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetProjectReferenceFromOutputDts implements checker.Program.
|
||||
func (r *aliasResolver) GetProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetProjectReferenceFromSource implements checker.Program.
|
||||
func (r *aliasResolver) GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetRedirectForResolution implements checker.Program.
|
||||
func (r *aliasResolver) GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetRedirectTargets implements checker.Program.
|
||||
func (r *aliasResolver) GetRedirectTargets(path tspath.Path) []string {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetResolvedModuleFromModuleSpecifier implements checker.Program.
|
||||
func (r *aliasResolver) GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) *module.ResolvedModule {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetSourceOfProjectReferenceIfOutputIncluded implements checker.Program.
|
||||
func (r *aliasResolver) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// IsSourceFileDefaultLibrary implements checker.Program.
|
||||
func (r *aliasResolver) IsSourceFileDefaultLibrary(path tspath.Path) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSourceFromProjectReference implements checker.Program.
|
||||
func (r *aliasResolver) IsSourceFromProjectReference(path tspath.Path) bool {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// SourceFileMayBeEmitted implements checker.Program.
|
||||
func (r *aliasResolver) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (r *aliasResolver) GetPackagesMap() map[string]bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ checker.Program = (*aliasResolver)(nil)
|
||||
@@ -0,0 +1,73 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/binder"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/module"
|
||||
"github.com/microsoft/typescript-go/internal/packagejson"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
)
|
||||
|
||||
type fakeCloneHost struct {
|
||||
fs vfs.FS
|
||||
}
|
||||
|
||||
func (h *fakeCloneHost) FS() vfs.FS { return h.fs }
|
||||
func (h *fakeCloneHost) GetCurrentDirectory() string { return "/" }
|
||||
func (h *fakeCloneHost) GetDefaultProject(path tspath.Path) (tspath.Path, *compiler.Program) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (h *fakeCloneHost) GetProgramForProject(projectPath tspath.Path) *compiler.Program { return nil }
|
||||
|
||||
func (h *fakeCloneHost) GetPackageJson(fileName string) *packagejson.InfoCacheEntry { return nil }
|
||||
|
||||
func (h *fakeCloneHost) GetSourceFile(fileName string, path tspath.Path) *ast.SourceFile { return nil }
|
||||
func (h *fakeCloneHost) Dispose() {}
|
||||
|
||||
var _ RegistryCloneHost = (*fakeCloneHost)(nil)
|
||||
|
||||
// Regression test for microsoft/typescript-go#4322.
|
||||
//
|
||||
// During auto-import export extraction, the checker is built on top of an
|
||||
// aliasResolver standing in for a real program. This file has a type error, and
|
||||
// extracting exports should still complete without crashing.
|
||||
func TestAliasResolverGetDiagnosticsDoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const fileName = "/pkg/index.ts"
|
||||
text := "declare function f(arg: { a: string }): () => void;\nexport const x = f({ a: 1 });\n"
|
||||
|
||||
fs := vfstest.FromMap(map[string]string{fileName: text}, true /*useCaseSensitiveFileNames*/)
|
||||
host := &fakeCloneHost{fs: fs}
|
||||
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: fileName,
|
||||
Path: tspath.Path(fileName),
|
||||
}, text, core.ScriptKindTS)
|
||||
binder.BindSourceFile(sourceFile)
|
||||
|
||||
resolver := module.NewResolver(host, core.EmptyCompilerOptions, "", "")
|
||||
r := newAliasResolver(
|
||||
[]*ast.SourceFile{sourceFile},
|
||||
nil,
|
||||
host,
|
||||
resolver,
|
||||
func(f string) tspath.Path { return tspath.Path(f) },
|
||||
func(ast.HasFileName, string) {},
|
||||
)
|
||||
|
||||
ch, _ := checker.NewChecker(r, nil)
|
||||
|
||||
// Type-checking this file's diagnostics must not panic.
|
||||
ch.GetDiagnostics(context.Background(), sourceFile)
|
||||
}
|
||||
142
tools/tsgo/internal/ls/autoimport/export.go
Normal file
142
tools/tsgo/internal/ls/autoimport/export.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
//go:generate go tool golang.org/x/tools/cmd/stringer -type=ExportSyntax -output=export_stringer_generated.go
|
||||
//go:generate npx dprint fmt export_stringer_generated.go
|
||||
|
||||
// ModuleID uniquely identifies a module across multiple declarations.
|
||||
// If the export is from an ambient module declaration, this is the module name.
|
||||
// If the export is from a module augmentation, this is the Path() of the resolved module file.
|
||||
// Otherwise this is the Path() of the exporting source file.
|
||||
type ModuleID string
|
||||
|
||||
type ExportID struct {
|
||||
ModuleID ModuleID
|
||||
ExportName string
|
||||
}
|
||||
|
||||
type ExportSyntax int
|
||||
|
||||
const (
|
||||
ExportSyntaxNone ExportSyntax = iota
|
||||
// export const x = {}
|
||||
ExportSyntaxModifier
|
||||
// export { x }
|
||||
ExportSyntaxNamed
|
||||
// export default function f() {}
|
||||
ExportSyntaxDefaultModifier
|
||||
// export default f
|
||||
ExportSyntaxDefaultDeclaration
|
||||
// export = x
|
||||
ExportSyntaxEquals
|
||||
// export as namespace x
|
||||
ExportSyntaxUMD
|
||||
// export * from "module"
|
||||
ExportSyntaxStar
|
||||
// module.exports = {}
|
||||
ExportSyntaxCommonJSModuleExports
|
||||
// exports.x = {}
|
||||
ExportSyntaxCommonJSExportsProperty
|
||||
)
|
||||
|
||||
type Export struct {
|
||||
ExportID
|
||||
ModuleFileName string
|
||||
Syntax ExportSyntax
|
||||
Flags ast.SymbolFlags
|
||||
localName string
|
||||
// through is the name of the module symbol's export that this export was found on,
|
||||
// either 'export=', InternalSymbolNameExportStar, or empty string.
|
||||
through string
|
||||
|
||||
// Checker-set fields
|
||||
|
||||
Target ExportID
|
||||
IsTypeOnly bool
|
||||
ScriptElementKind lsutil.ScriptElementKind
|
||||
ScriptElementKindModifiers lsutil.ScriptElementKindModifier
|
||||
|
||||
// The file where the export was found.
|
||||
Path tspath.Path
|
||||
|
||||
PackageName string
|
||||
}
|
||||
|
||||
func (e *Export) Name() string {
|
||||
if e.localName != "" {
|
||||
return e.localName
|
||||
}
|
||||
if e.ExportName == ast.InternalSymbolNameExportEquals {
|
||||
return e.Target.ExportName
|
||||
}
|
||||
return e.ExportName
|
||||
}
|
||||
|
||||
func (e *Export) IsRenameable() bool {
|
||||
return e.ExportName == ast.InternalSymbolNameExportEquals || e.ExportName == ast.InternalSymbolNameDefault
|
||||
}
|
||||
|
||||
func (e *Export) AmbientModuleName() string {
|
||||
if !tspath.IsExternalModuleNameRelative(string(e.ModuleID)) {
|
||||
return string(e.ModuleID)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (e *Export) IsUnresolvedAlias() bool {
|
||||
return e.Flags == ast.SymbolFlagsAlias
|
||||
}
|
||||
|
||||
func SymbolToExport(symbol *ast.Symbol, ch *checker.Checker) *Export {
|
||||
if symbol.Parent != nil && checker.IsExternalModuleSymbol(symbol.Parent) {
|
||||
if moduleID, moduleFileName, ok := tryGetModuleIDAndFileNameOfModuleSymbol(symbol.Parent); ok {
|
||||
return extractFirstExport(symbol, ch, moduleID, moduleFileName, ast.GetSourceFileOfModule(symbol.Parent))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
declaration := core.FirstOrNil(symbol.Declarations)
|
||||
if declaration == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
file := ast.GetSourceFileOfNode(declaration)
|
||||
if file.Symbol == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
moduleSymbol := ch.GetMergedSymbol(file.Symbol)
|
||||
moduleID := ModuleID(file.Path())
|
||||
moduleFileName := file.FileName()
|
||||
target := ch.GetMergedSymbol(ch.SkipAlias(symbol))
|
||||
|
||||
if export := tryGetModuleExport(ast.InternalSymbolNameDefault, target, moduleSymbol, ch, moduleID, moduleFileName, file); export != nil {
|
||||
return export
|
||||
}
|
||||
if export := tryGetModuleExport(ast.InternalSymbolNameExportEquals, target, moduleSymbol, ch, moduleID, moduleFileName, file); export != nil {
|
||||
return export
|
||||
}
|
||||
return tryGetModuleExport(symbol.Name, target, moduleSymbol, ch, moduleID, moduleFileName, file)
|
||||
}
|
||||
|
||||
func tryGetModuleExport(exportName string, target *ast.Symbol, moduleSymbol *ast.Symbol, ch *checker.Checker, moduleID ModuleID, moduleFileName string, file *ast.SourceFile) *Export {
|
||||
exported := ch.TryGetMemberInModuleExportsAndProperties(exportName, moduleSymbol)
|
||||
if exported != nil && ch.GetMergedSymbol(ch.SkipAlias(exported)) == target {
|
||||
return extractFirstExport(exported, ch, moduleID, moduleFileName, file)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractFirstExport(symbol *ast.Symbol, ch *checker.Checker, moduleID ModuleID, moduleFileName string, file *ast.SourceFile) *Export {
|
||||
var exports []*Export
|
||||
extractor := newSymbolExtractor("", ch, nil, nil)
|
||||
extractor.extractFromSymbol(symbol.Name, symbol, moduleID, moduleFileName, file, &exports)
|
||||
return core.FirstOrNil(exports)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Code generated by "stringer -type=ExportSyntax -output=export_stringer_generated.go"; DO NOT EDIT.
|
||||
|
||||
package autoimport
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[ExportSyntaxNone-0]
|
||||
_ = x[ExportSyntaxModifier-1]
|
||||
_ = x[ExportSyntaxNamed-2]
|
||||
_ = x[ExportSyntaxDefaultModifier-3]
|
||||
_ = x[ExportSyntaxDefaultDeclaration-4]
|
||||
_ = x[ExportSyntaxEquals-5]
|
||||
_ = x[ExportSyntaxUMD-6]
|
||||
_ = x[ExportSyntaxStar-7]
|
||||
_ = x[ExportSyntaxCommonJSModuleExports-8]
|
||||
_ = x[ExportSyntaxCommonJSExportsProperty-9]
|
||||
}
|
||||
|
||||
const _ExportSyntax_name = "ExportSyntaxNoneExportSyntaxModifierExportSyntaxNamedExportSyntaxDefaultModifierExportSyntaxDefaultDeclarationExportSyntaxEqualsExportSyntaxUMDExportSyntaxStarExportSyntaxCommonJSModuleExportsExportSyntaxCommonJSExportsProperty"
|
||||
|
||||
var _ExportSyntax_index = [...]uint8{0, 16, 36, 53, 80, 110, 128, 143, 159, 192, 227}
|
||||
|
||||
func (i ExportSyntax) String() string {
|
||||
idx := int(i) - 0
|
||||
if i < 0 || idx >= len(_ExportSyntax_index)-1 {
|
||||
return "ExportSyntax(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _ExportSyntax_name[_ExportSyntax_index[idx]:_ExportSyntax_index[idx+1]]
|
||||
}
|
||||
461
tools/tsgo/internal/ls/autoimport/extract.go
Normal file
461
tools/tsgo/internal/ls/autoimport/extract.go
Normal file
@@ -0,0 +1,461 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/binder"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/module"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
type symbolExtractor struct {
|
||||
packageName string
|
||||
stats *extractorStats
|
||||
|
||||
localNameResolver *binder.NameResolver
|
||||
checker *checker.Checker
|
||||
toPath func(fileName string) tspath.Path
|
||||
// realpath, if set, is used to resolve symlinks for ModuleID generation.
|
||||
// This ensures that symlinked packages use their realpath as ModuleID,
|
||||
// deduplicating exports from files that appear via multiple symlink paths.
|
||||
realpath func(fileName string) string
|
||||
}
|
||||
|
||||
type exportExtractor struct {
|
||||
*symbolExtractor
|
||||
moduleResolver *module.Resolver
|
||||
}
|
||||
|
||||
type extractorStats struct {
|
||||
exports atomic.Int32
|
||||
usedChecker atomic.Int32
|
||||
}
|
||||
|
||||
func (e *exportExtractor) Stats() *extractorStats {
|
||||
return e.stats
|
||||
}
|
||||
|
||||
type checkerLease struct {
|
||||
used bool
|
||||
checker *checker.Checker
|
||||
}
|
||||
|
||||
func (l *checkerLease) GetChecker() *checker.Checker {
|
||||
l.used = true
|
||||
return l.checker
|
||||
}
|
||||
|
||||
func (l *checkerLease) TryChecker() *checker.Checker {
|
||||
if l.used {
|
||||
return l.checker
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newSymbolExtractor(packageName string, checker *checker.Checker, toPath func(string) tspath.Path, realpath func(string) string) *symbolExtractor {
|
||||
return &symbolExtractor{
|
||||
packageName: packageName,
|
||||
checker: checker,
|
||||
localNameResolver: &binder.NameResolver{
|
||||
CompilerOptions: core.EmptyCompilerOptions,
|
||||
},
|
||||
stats: &extractorStats{},
|
||||
toPath: toPath,
|
||||
realpath: realpath,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *registryBuilder) newExportExtractor(packageName string, checker *checker.Checker, moduleResolver *module.Resolver, realpath func(string) string) *exportExtractor {
|
||||
return &exportExtractor{
|
||||
symbolExtractor: newSymbolExtractor(packageName, checker, b.base.toPath, realpath),
|
||||
moduleResolver: moduleResolver,
|
||||
}
|
||||
}
|
||||
|
||||
// getModuleID returns the ModuleID for a file, using realpath if available.
|
||||
func (e *symbolExtractor) getModuleID(file *ast.SourceFile) ModuleID {
|
||||
if e.realpath != nil && e.toPath != nil {
|
||||
realpath := e.realpath(file.FileName())
|
||||
return ModuleID(e.toPath(realpath))
|
||||
}
|
||||
return ModuleID(file.Path())
|
||||
}
|
||||
|
||||
// getModuleIDForSymbol returns the ModuleID for a module symbol, using realpath
|
||||
// normalization when available for source files.
|
||||
func (e *symbolExtractor) getModuleIDForSymbol(symbol *ast.Symbol) (ModuleID, bool) {
|
||||
moduleID, fileName, ok := tryGetModuleIDAndFileNameOfModuleSymbol(symbol)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
// If fileName is set, this is a source file that may need realpath normalization
|
||||
if fileName != "" && e.realpath != nil {
|
||||
decl := ast.GetNonAugmentationDeclaration(symbol)
|
||||
if decl != nil && decl.Kind == ast.KindSourceFile {
|
||||
return e.getModuleID(decl.AsSourceFile()), true
|
||||
}
|
||||
}
|
||||
return moduleID, true
|
||||
}
|
||||
|
||||
func (e *exportExtractor) extractFromFile(file *ast.SourceFile) []*Export {
|
||||
if file.Symbol != nil {
|
||||
return e.extractFromModule(file)
|
||||
}
|
||||
if len(file.AmbientModuleNames) > 0 {
|
||||
moduleDeclarations := core.Filter(file.Statements.Nodes, ast.IsModuleWithStringLiteralName)
|
||||
var exportCount int
|
||||
for _, decl := range moduleDeclarations {
|
||||
exportCount += len(decl.AsModuleDeclaration().Symbol.Exports)
|
||||
}
|
||||
exports := make([]*Export, 0, exportCount)
|
||||
for _, decl := range moduleDeclarations {
|
||||
e.extractFromModuleDeclaration(decl.AsModuleDeclaration(), file, ModuleID(decl.Name().Text()), "", &exports)
|
||||
}
|
||||
return exports
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *exportExtractor) extractFromModule(file *ast.SourceFile) []*Export {
|
||||
moduleAugmentations := core.MapNonNil(file.ModuleAugmentations, func(name *ast.ModuleName) *ast.ModuleDeclaration {
|
||||
decl := name.Parent
|
||||
if ast.IsGlobalScopeAugmentation(decl) {
|
||||
return nil
|
||||
}
|
||||
return decl.AsModuleDeclaration()
|
||||
})
|
||||
var augmentationExportCount int
|
||||
for _, decl := range moduleAugmentations {
|
||||
augmentationExportCount += len(decl.Symbol.Exports)
|
||||
}
|
||||
moduleID := e.getModuleID(file)
|
||||
exports := make([]*Export, 0, len(file.Symbol.Exports)+augmentationExportCount)
|
||||
for name, symbol := range file.Symbol.Exports {
|
||||
e.extractFromSymbol(name, symbol, moduleID, file.FileName(), file, &exports)
|
||||
}
|
||||
for _, decl := range moduleAugmentations {
|
||||
name := decl.Name().AsStringLiteral().Text
|
||||
moduleID := ModuleID(name)
|
||||
var moduleFileName string
|
||||
if tspath.IsExternalModuleNameRelative(name) {
|
||||
if resolved, _ := e.moduleResolver.ResolveModuleName(name, file.FileName(), core.ModuleKindCommonJS, nil); resolved.IsResolved() {
|
||||
moduleFileName = resolved.ResolvedFileName
|
||||
moduleID = ModuleID(e.toPath(moduleFileName))
|
||||
} else {
|
||||
// :shrug:
|
||||
moduleFileName = tspath.ResolvePath(tspath.GetDirectoryPath(file.FileName()), name)
|
||||
moduleID = ModuleID(e.toPath(moduleFileName))
|
||||
}
|
||||
}
|
||||
e.extractFromModuleDeclaration(decl, file, moduleID, moduleFileName, &exports)
|
||||
}
|
||||
return exports
|
||||
}
|
||||
|
||||
func (e *exportExtractor) extractFromModuleDeclaration(decl *ast.ModuleDeclaration, file *ast.SourceFile, moduleID ModuleID, moduleFileName string, exports *[]*Export) {
|
||||
for name, symbol := range decl.Symbol.Exports {
|
||||
e.extractFromSymbol(name, symbol, moduleID, moduleFileName, file, exports)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *symbolExtractor) extractFromSymbol(name string, symbol *ast.Symbol, moduleID ModuleID, moduleFileName string, file *ast.SourceFile, exports *[]*Export) {
|
||||
if shouldIgnoreSymbol(symbol) {
|
||||
return
|
||||
}
|
||||
|
||||
if name == ast.InternalSymbolNameExportStar {
|
||||
checkerLease := &checkerLease{checker: e.checker}
|
||||
allExports := e.checker.GetExportsOfModule(symbol.Parent)
|
||||
// allExports includes named exports from the file that will be processed separately;
|
||||
// we want to add only the ones that come from the star
|
||||
for name, namedExport := range symbol.Parent.Exports {
|
||||
if name != ast.InternalSymbolNameExportStar {
|
||||
idx := slices.Index(allExports, namedExport)
|
||||
if idx >= 0 || shouldIgnoreSymbol(namedExport) {
|
||||
allExports = slices.Delete(allExports, idx, idx+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*exports = slices.Grow(*exports, len(allExports))
|
||||
for _, reexportedSymbol := range allExports {
|
||||
export, _ := e.createExport(reexportedSymbol, moduleID, moduleFileName, ExportSyntaxStar, file, checkerLease)
|
||||
if export != nil {
|
||||
parent := checkerLease.GetChecker().GetMergedSymbol(reexportedSymbol.Parent)
|
||||
if parent != nil && parent.IsExternalModule() {
|
||||
if targetModuleID, ok := e.getModuleIDForSymbol(parent); ok {
|
||||
export.Target = ExportID{
|
||||
ExportName: reexportedSymbol.Name,
|
||||
ModuleID: targetModuleID,
|
||||
}
|
||||
}
|
||||
}
|
||||
export.through = ast.InternalSymbolNameExportStar
|
||||
*exports = append(*exports, export)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
syntax := getSyntax(symbol)
|
||||
checkerLease := &checkerLease{checker: e.checker}
|
||||
export, target := e.createExport(symbol, moduleID, moduleFileName, syntax, file, checkerLease)
|
||||
if export == nil {
|
||||
return
|
||||
}
|
||||
|
||||
*exports = append(*exports, export)
|
||||
|
||||
if target != nil {
|
||||
if syntax == ExportSyntaxEquals && target.Flags&ast.SymbolFlagsNamespace != 0 {
|
||||
*exports = slices.Grow(*exports, len(target.Exports))
|
||||
for innerName, namedExport := range target.Exports {
|
||||
if innerName != ast.InternalSymbolNameExportStar {
|
||||
export, _ := e.createExport(namedExport, moduleID, moduleFileName, syntax, file, checkerLease)
|
||||
if export != nil {
|
||||
export.through = name
|
||||
*exports = append(*exports, export)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if syntax == ExportSyntaxCommonJSModuleExports {
|
||||
expression := symbol.Declarations[0].AsBinaryExpression().Right
|
||||
if expression.Kind == ast.KindObjectLiteralExpression {
|
||||
// what is actually desirable here? I think it would be reasonable to only treat these as exports
|
||||
// if *every* property is a shorthand property or identifier: identifier
|
||||
// At least, it would be sketchy if there were any methods, computed properties...
|
||||
*exports = slices.Grow(*exports, len(expression.AsObjectLiteralExpression().Properties.Nodes))
|
||||
for _, prop := range expression.AsObjectLiteralExpression().Properties.Nodes {
|
||||
if ast.IsShorthandPropertyAssignment(prop) || ast.IsPropertyAssignment(prop) && prop.AsPropertyAssignment().Name().Kind == ast.KindIdentifier {
|
||||
export, _ := e.createExport(expression.Symbol().Members[prop.Name().Text()], moduleID, moduleFileName, syntax, file, checkerLease)
|
||||
if export != nil {
|
||||
export.through = name
|
||||
*exports = append(*exports, export)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// createExport creates an Export for the given symbol, returning the Export and the target symbol if the export is an alias.
|
||||
func (e *symbolExtractor) createExport(symbol *ast.Symbol, moduleID ModuleID, moduleFileName string, syntax ExportSyntax, file *ast.SourceFile, checkerLease *checkerLease) (*Export, *ast.Symbol) {
|
||||
if shouldIgnoreSymbol(symbol) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
export := &Export{
|
||||
ExportID: ExportID{
|
||||
ExportName: symbol.Name,
|
||||
ModuleID: moduleID,
|
||||
},
|
||||
ModuleFileName: moduleFileName,
|
||||
Syntax: syntax,
|
||||
Flags: symbol.CombinedLocalAndExportSymbolFlags(),
|
||||
Path: file.Path(),
|
||||
PackageName: e.packageName,
|
||||
}
|
||||
|
||||
if syntax == ExportSyntaxUMD {
|
||||
export.ExportName = ast.InternalSymbolNameExportEquals
|
||||
export.localName = symbol.Name
|
||||
}
|
||||
|
||||
var targetSymbol *ast.Symbol
|
||||
if symbol.Flags&ast.SymbolFlagsAlias != 0 {
|
||||
targetSymbol = e.tryResolveSymbol(symbol, syntax, checkerLease)
|
||||
if targetSymbol != nil {
|
||||
var decl *ast.Node
|
||||
if len(targetSymbol.Declarations) > 0 {
|
||||
decl = targetSymbol.Declarations[0]
|
||||
} else if targetSymbol.CheckFlags&ast.CheckFlagsMapped != 0 {
|
||||
if mappedDecl := checkerLease.GetChecker().GetMappedTypeSymbolOfProperty(targetSymbol); mappedDecl != nil && len(mappedDecl.Declarations) > 0 {
|
||||
decl = mappedDecl.Declarations[0]
|
||||
}
|
||||
}
|
||||
if decl == nil {
|
||||
// !!! consider GetImmediateAliasedSymbol to go as far as we can
|
||||
decl = symbol.Declarations[0]
|
||||
}
|
||||
if decl == nil {
|
||||
panic("no declaration for aliased symbol")
|
||||
}
|
||||
|
||||
parent := targetSymbol.Parent
|
||||
if checker := checkerLease.TryChecker(); checker != nil {
|
||||
export.Flags = checker.GetSymbolFlags(targetSymbol)
|
||||
export.IsTypeOnly = checker.GetTypeOnlyAliasDeclaration(symbol) != nil
|
||||
parent = checker.GetMergedSymbol(parent)
|
||||
} else {
|
||||
export.Flags = targetSymbol.Flags
|
||||
export.IsTypeOnly = core.Some(symbol.Declarations, ast.IsPartOfTypeOnlyImportOrExportDeclaration)
|
||||
}
|
||||
export.ScriptElementKind = lsutil.GetSymbolKind(checkerLease.TryChecker(), targetSymbol, decl)
|
||||
export.ScriptElementKindModifiers = lsutil.GetSymbolModifiers(checkerLease.TryChecker(), targetSymbol)
|
||||
targetModuleID := ModuleID(ast.GetSourceFileOfNode(decl).Path())
|
||||
if parent != nil && parent.IsExternalModule() {
|
||||
if id, ok := e.getModuleIDForSymbol(parent); ok {
|
||||
targetModuleID = id
|
||||
}
|
||||
}
|
||||
export.Target = ExportID{
|
||||
ExportName: targetSymbol.Name,
|
||||
ModuleID: targetModuleID,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
export.ScriptElementKind = lsutil.GetSymbolKind(checkerLease.TryChecker(), symbol, symbol.Declarations[0])
|
||||
export.ScriptElementKindModifiers = lsutil.GetSymbolModifiers(checkerLease.TryChecker(), symbol)
|
||||
}
|
||||
|
||||
if symbol.Name == ast.InternalSymbolNameDefault || symbol.Name == ast.InternalSymbolNameExportEquals {
|
||||
namedSymbol := symbol
|
||||
if s := binder.GetLocalSymbolForExportDefault(symbol); s != nil {
|
||||
namedSymbol = s
|
||||
}
|
||||
export.localName = getDefaultLikeExportNameFromDeclaration(namedSymbol)
|
||||
if isUnusableName(export.localName) {
|
||||
export.localName = export.Target.ExportName
|
||||
}
|
||||
if isUnusableName(export.localName) {
|
||||
if targetSymbol != nil {
|
||||
namedSymbol = targetSymbol
|
||||
if s := binder.GetLocalSymbolForExportDefault(targetSymbol); s != nil {
|
||||
namedSymbol = s
|
||||
}
|
||||
export.localName = getDefaultLikeExportNameFromDeclaration(namedSymbol)
|
||||
}
|
||||
}
|
||||
if isUnusableName(export.localName) {
|
||||
// Last resort: derive identifier from the file name. Use FileName() (original
|
||||
// casing) rather than ModuleID/Path() which is lowercased on case-insensitive
|
||||
// file systems, losing PascalCase.
|
||||
export.localName = lsutil.ModuleSpecifierToValidIdentifier(fileNameForDefaultExportName(targetSymbol, moduleFileName, moduleID), false)
|
||||
}
|
||||
}
|
||||
|
||||
if isUnusableName(export.Name()) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
e.stats.exports.Add(1)
|
||||
if checkerLease.TryChecker() != nil {
|
||||
e.stats.usedChecker.Add(1)
|
||||
}
|
||||
|
||||
return export, targetSymbol
|
||||
}
|
||||
|
||||
func (e *symbolExtractor) tryResolveSymbol(symbol *ast.Symbol, syntax ExportSyntax, checkerLease *checkerLease) *ast.Symbol {
|
||||
if !ast.IsNonLocalAlias(symbol, ast.SymbolFlagsNone) {
|
||||
return symbol
|
||||
}
|
||||
|
||||
var loc *ast.Node
|
||||
var name string
|
||||
switch syntax {
|
||||
case ExportSyntaxNamed:
|
||||
decl := ast.GetDeclarationOfKind(symbol, ast.KindExportSpecifier)
|
||||
if decl.Parent.Parent.AsExportDeclaration().ModuleSpecifier == nil {
|
||||
if n := core.FirstNonZero(decl.Name(), decl.PropertyName()); n.Kind == ast.KindIdentifier {
|
||||
loc = n
|
||||
name = n.Text()
|
||||
}
|
||||
}
|
||||
// !!! check if module.exports = foo is marked as an alias
|
||||
case ExportSyntaxEquals:
|
||||
if symbol.Name != ast.InternalSymbolNameExportEquals {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case ExportSyntaxDefaultDeclaration:
|
||||
decl := ast.GetDeclarationOfKind(symbol, ast.KindExportAssignment)
|
||||
if decl.Expression().Kind == ast.KindIdentifier {
|
||||
loc = decl.Expression()
|
||||
name = loc.Text()
|
||||
}
|
||||
}
|
||||
|
||||
if loc != nil {
|
||||
local := e.localNameResolver.Resolve(loc, name, ast.SymbolFlagsAll, nil, false, false)
|
||||
if local != nil && !ast.IsNonLocalAlias(local, ast.SymbolFlagsNone) {
|
||||
return local
|
||||
}
|
||||
}
|
||||
|
||||
checker := checkerLease.GetChecker()
|
||||
if resolved := checker.GetAliasedSymbol(symbol); !checker.IsUnknownSymbol(resolved) {
|
||||
return resolved
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldIgnoreSymbol(symbol *ast.Symbol) bool {
|
||||
if symbol.Flags&ast.SymbolFlagsPrototype != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getSyntax(symbol *ast.Symbol) ExportSyntax {
|
||||
for _, decl := range symbol.Declarations {
|
||||
switch decl.Kind {
|
||||
case ast.KindExportSpecifier:
|
||||
return ExportSyntaxNamed
|
||||
case ast.KindExportAssignment:
|
||||
return core.IfElse(
|
||||
decl.AsExportAssignment().IsExportEquals,
|
||||
ExportSyntaxEquals,
|
||||
ExportSyntaxDefaultDeclaration,
|
||||
)
|
||||
case ast.KindNamespaceExportDeclaration:
|
||||
return ExportSyntaxUMD
|
||||
case ast.KindBinaryExpression:
|
||||
switch ast.GetAssignmentDeclarationKind(decl) {
|
||||
case ast.JSDeclarationKindModuleExports:
|
||||
return ExportSyntaxCommonJSModuleExports
|
||||
case ast.JSDeclarationKindExportsProperty:
|
||||
return ExportSyntaxCommonJSExportsProperty
|
||||
}
|
||||
default:
|
||||
if ast.GetCombinedModifierFlags(decl)&ast.ModifierFlagsDefault != 0 {
|
||||
return ExportSyntaxDefaultModifier
|
||||
} else {
|
||||
return ExportSyntaxModifier
|
||||
}
|
||||
}
|
||||
}
|
||||
return ExportSyntaxNone
|
||||
}
|
||||
|
||||
func isUnusableName(name string) bool {
|
||||
return name == "" ||
|
||||
name == "_default" ||
|
||||
name == ast.InternalSymbolNameExportStar ||
|
||||
name == ast.InternalSymbolNameDefault ||
|
||||
name == ast.InternalSymbolNameExportEquals
|
||||
}
|
||||
|
||||
// fileNameForDefaultExportName returns the best file name to use when deriving
|
||||
// a fallback identifier for a default-like export. It prefers the target symbol's
|
||||
// source file (closest to the export origin), falls back to the module's original
|
||||
// file name, and uses the lowercased moduleID only for ambient modules where no
|
||||
// original file name is available.
|
||||
func fileNameForDefaultExportName(targetSymbol *ast.Symbol, moduleFileName string, moduleID ModuleID) string {
|
||||
if targetSymbol != nil && len(targetSymbol.Declarations) > 0 {
|
||||
if fn := ast.GetSourceFileOfNode(targetSymbol.Declarations[0]).FileName(); fn != "" {
|
||||
return fn
|
||||
}
|
||||
}
|
||||
if moduleFileName != "" {
|
||||
return moduleFileName
|
||||
}
|
||||
return string(moduleID)
|
||||
}
|
||||
1314
tools/tsgo/internal/ls/autoimport/fix.go
Normal file
1314
tools/tsgo/internal/ls/autoimport/fix.go
Normal file
File diff suppressed because it is too large
Load Diff
501
tools/tsgo/internal/ls/autoimport/import_adder.go
Normal file
501
tools/tsgo/internal/ls/autoimport/import_adder.go
Normal file
@@ -0,0 +1,501 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/debug"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
|
||||
// "github.com/microsoft/typescript-go/internal/ls"
|
||||
"github.com/microsoft/typescript-go/internal/ls/change"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/nodebuilder"
|
||||
)
|
||||
|
||||
type ImportAdder interface {
|
||||
HasFixes() bool
|
||||
AddImportFromExportedSymbol(symbol *ast.Symbol, isValidTypeOnlyUseSite bool)
|
||||
AddImportFix(fix *Fix)
|
||||
Edits() []*lsproto.TextEdit
|
||||
}
|
||||
|
||||
// addToExistingState tracks modifications to an existing import clause or binding pattern
|
||||
type addToExistingState struct {
|
||||
importClauseOrBindingPattern *ast.ImportClauseOrBindingPattern
|
||||
defaultImport *newImportBinding
|
||||
namedImports map[string]*newImportBinding
|
||||
}
|
||||
|
||||
// importsCollection tracks new imports to be created for a given module specifier
|
||||
type importsCollection struct {
|
||||
defaultImport *newImportBinding
|
||||
namedImports map[string]*newImportBinding
|
||||
namespaceLikeImport *newImportBinding
|
||||
useRequire bool
|
||||
}
|
||||
|
||||
func newImportsKey(moduleSpecifier string, topLevelTypeOnly bool) string {
|
||||
if topLevelTypeOnly {
|
||||
return "1|" + moduleSpecifier
|
||||
}
|
||||
return "0|" + moduleSpecifier
|
||||
}
|
||||
|
||||
type importAdder struct {
|
||||
// Context
|
||||
ctx context.Context
|
||||
checker *checker.Checker
|
||||
view *View
|
||||
formatOptions lsutil.FormatCodeSettings
|
||||
converters *lsconv.Converters
|
||||
preferences lsutil.UserPreferences
|
||||
|
||||
// State
|
||||
addToNamespace []*Fix // Namespace fixes don't conflict, so just build a list
|
||||
importType []*Fix // JSDoc type import fixes
|
||||
addToExisting map[*ast.ImportClauseOrBindingPattern]*addToExistingState // importClauseOrBindingPattern -> default or named bindings
|
||||
newImports map[string]*importsCollection // module specifier + type only -> imports
|
||||
// !!! removeExisting, verbatimImports?
|
||||
}
|
||||
|
||||
func NewImportAdder(
|
||||
ctx context.Context,
|
||||
program *compiler.Program,
|
||||
checker *checker.Checker,
|
||||
file *ast.SourceFile,
|
||||
view *View,
|
||||
formatOptions lsutil.FormatCodeSettings,
|
||||
converters *lsconv.Converters,
|
||||
preferences lsutil.UserPreferences,
|
||||
) ImportAdder {
|
||||
return &importAdder{
|
||||
ctx: ctx,
|
||||
checker: checker,
|
||||
view: view,
|
||||
formatOptions: formatOptions,
|
||||
converters: converters,
|
||||
preferences: preferences,
|
||||
addToNamespace: nil,
|
||||
importType: nil,
|
||||
addToExisting: make(map[*ast.Node]*addToExistingState),
|
||||
newImports: make(map[string]*importsCollection),
|
||||
}
|
||||
}
|
||||
|
||||
func (adder *importAdder) HasFixes() bool {
|
||||
return len(adder.addToNamespace) > 0 ||
|
||||
len(adder.importType) > 0 ||
|
||||
len(adder.addToExisting) > 0 ||
|
||||
len(adder.newImports) > 0
|
||||
}
|
||||
|
||||
// !!! referenceImport
|
||||
func (adder *importAdder) AddImportFromExportedSymbol(exportedSymbol *ast.Symbol, isValidTypeOnlyUseSite bool) {
|
||||
symbol := adder.checker.GetMergedSymbol(adder.checker.SkipAlias(exportedSymbol))
|
||||
exportInfos := adder.getAllExportsForSymbol(symbol)
|
||||
if len(exportInfos) == 0 {
|
||||
// If no exportInfo is found, this means export could not be resolved when we have filtered for autoImportFileExcludePatterns,
|
||||
// so we should not generate an import.
|
||||
// debug.Assert(len(adder.ls.UserPreferences().AutoImportFileExcludePatterns) > 0)
|
||||
return
|
||||
}
|
||||
fix := adder.getImportFixForSymbol(adder.view, adder.view.importingFile, exportInfos, isValidTypeOnlyUseSite)
|
||||
if fix != nil {
|
||||
// !!! referenceImport -> propertyName
|
||||
adder.AddImportFix(fix)
|
||||
}
|
||||
}
|
||||
|
||||
func (adder *importAdder) Edits() []*lsproto.TextEdit {
|
||||
// !!! organize imports?
|
||||
tracker := change.NewTracker(adder.ctx, adder.view.program.Options(), adder.formatOptions, adder.converters)
|
||||
quotePreference := lsutil.GetQuotePreference(adder.view.importingFile, adder.preferences)
|
||||
for _, fix := range adder.addToNamespace {
|
||||
addNamespaceQualifier(fix, tracker, adder.view.importingFile, locale.Default)
|
||||
}
|
||||
for _, fix := range adder.importType {
|
||||
addImportType(fix, adder.view.importingFile, adder.preferences, tracker, locale.Default)
|
||||
}
|
||||
for clauseOrPattern, entry := range adder.addToExisting {
|
||||
addToExistingImport(
|
||||
tracker,
|
||||
adder.view.importingFile,
|
||||
clauseOrPattern,
|
||||
entry.defaultImport,
|
||||
sortedNamedImports(entry.namedImports),
|
||||
adder.preferences,
|
||||
)
|
||||
}
|
||||
|
||||
var newDeclarations []*ast.AnyImportOrRequireStatement
|
||||
for key, newImport := range adder.newImports {
|
||||
moduleSpecifier := key[2:] // From `${0 | 1}|${moduleSpecifier}` format
|
||||
var declarations []*ast.AnyImportOrRequireStatement
|
||||
if newImport.useRequire {
|
||||
declarations = getNewRequires(
|
||||
tracker,
|
||||
moduleSpecifier,
|
||||
quotePreference,
|
||||
newImport.defaultImport,
|
||||
sortedNamedImports(newImport.namedImports),
|
||||
newImport.namespaceLikeImport,
|
||||
adder.view.program.Options(),
|
||||
)
|
||||
} else {
|
||||
declarations = getNewImports(
|
||||
tracker,
|
||||
moduleSpecifier,
|
||||
quotePreference,
|
||||
newImport.defaultImport,
|
||||
sortedNamedImports(newImport.namedImports),
|
||||
newImport.namespaceLikeImport,
|
||||
adder.view.program.Options(),
|
||||
adder.preferences,
|
||||
)
|
||||
}
|
||||
newDeclarations = append(newDeclarations, declarations...)
|
||||
}
|
||||
|
||||
if len(newDeclarations) > 0 {
|
||||
insertImports(tracker, adder.view.importingFile, newDeclarations, true /*blankLineBetween*/, adder.preferences)
|
||||
}
|
||||
|
||||
return tracker.GetChanges()[adder.view.importingFile.FileName()]
|
||||
}
|
||||
|
||||
func sortedNamedImports(m map[string]*newImportBinding) []*newImportBinding {
|
||||
keys := slices.Sorted(maps.Keys(m))
|
||||
result := make([]*newImportBinding, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
result = append(result, m[k])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// AddImportFix adds a fix to the import adder, accumulating it with other fixes
|
||||
// so that multiple imports from the same module are coalesced into a single import statement.
|
||||
func (adder *importAdder) AddImportFix(fix *Fix) {
|
||||
symbolName := fix.Name
|
||||
compilerOptions := adder.view.program.Options()
|
||||
|
||||
switch fix.Kind {
|
||||
case lsproto.AutoImportFixKindUseNamespace:
|
||||
adder.addToNamespace = append(adder.addToNamespace, fix)
|
||||
case lsproto.AutoImportFixKindJsdocTypeImport:
|
||||
adder.importType = append(adder.importType, fix)
|
||||
case lsproto.AutoImportFixKindAddToExisting:
|
||||
existingFix := getAddToExistingImportFix(adder.view.importingFile, fix)
|
||||
entry := adder.addToExisting[existingFix.importClauseOrBindingPattern]
|
||||
if entry == nil {
|
||||
entry = &addToExistingState{
|
||||
importClauseOrBindingPattern: existingFix.importClauseOrBindingPattern,
|
||||
namedImports: make(map[string]*newImportBinding),
|
||||
}
|
||||
adder.addToExisting[existingFix.importClauseOrBindingPattern] = entry
|
||||
}
|
||||
|
||||
if fix.ImportKind == lsproto.ImportKindNamed {
|
||||
prevImport := entry.namedImports[symbolName]
|
||||
var prevTypeOnly lsproto.AddAsTypeOnly
|
||||
if prevImport != nil {
|
||||
prevTypeOnly = prevImport.addAsTypeOnly
|
||||
}
|
||||
entry.namedImports[symbolName] = &newImportBinding{
|
||||
kind: lsproto.ImportKindNamed,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
|
||||
propertyName: existingFix.namedImport.propertyName,
|
||||
}
|
||||
} else {
|
||||
// Default import
|
||||
debug.Assert(
|
||||
entry.defaultImport == nil || entry.defaultImport.name == symbolName,
|
||||
"(Add to Existing) Default import should be missing or match symbolName",
|
||||
)
|
||||
var prevTypeOnly lsproto.AddAsTypeOnly
|
||||
if entry.defaultImport != nil {
|
||||
prevTypeOnly = entry.defaultImport.addAsTypeOnly
|
||||
}
|
||||
entry.defaultImport = &newImportBinding{
|
||||
kind: lsproto.ImportKindDefault,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
|
||||
}
|
||||
}
|
||||
|
||||
case lsproto.AutoImportFixKindAddNew:
|
||||
entry := adder.getNewImportEntry(fix.ModuleSpecifier, fix.ImportKind, fix.UseRequire, fix.AddAsTypeOnly)
|
||||
debug.Assert(
|
||||
entry.useRequire == fix.UseRequire,
|
||||
"(Add new) Tried to add an `import` and a `require` for the same module",
|
||||
)
|
||||
|
||||
switch fix.ImportKind {
|
||||
case lsproto.ImportKindDefault:
|
||||
debug.Assert(
|
||||
entry.defaultImport == nil || entry.defaultImport.name == symbolName,
|
||||
"(Add new) Default import should be missing or match symbolName",
|
||||
)
|
||||
var prevTypeOnly lsproto.AddAsTypeOnly
|
||||
if entry.defaultImport != nil {
|
||||
prevTypeOnly = entry.defaultImport.addAsTypeOnly
|
||||
}
|
||||
entry.defaultImport = &newImportBinding{
|
||||
kind: lsproto.ImportKindDefault,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
|
||||
}
|
||||
|
||||
case lsproto.ImportKindNamed:
|
||||
if entry.namedImports == nil {
|
||||
entry.namedImports = make(map[string]*newImportBinding)
|
||||
}
|
||||
prevImport := entry.namedImports[symbolName]
|
||||
var prevTypeOnly lsproto.AddAsTypeOnly
|
||||
if prevImport != nil {
|
||||
prevTypeOnly = prevImport.addAsTypeOnly
|
||||
}
|
||||
entry.namedImports[symbolName] = &newImportBinding{
|
||||
kind: lsproto.ImportKindNamed,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
|
||||
// !!! propertyName
|
||||
}
|
||||
|
||||
case lsproto.ImportKindCommonJS:
|
||||
if compilerOptions.VerbatimModuleSyntax == core.TSTrue {
|
||||
if entry.namedImports == nil {
|
||||
entry.namedImports = make(map[string]*newImportBinding)
|
||||
}
|
||||
prevImport := entry.namedImports[symbolName]
|
||||
var prevTypeOnly lsproto.AddAsTypeOnly
|
||||
if prevImport != nil {
|
||||
prevTypeOnly = prevImport.addAsTypeOnly
|
||||
}
|
||||
entry.namedImports[symbolName] = &newImportBinding{
|
||||
kind: lsproto.ImportKindCommonJS,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, fix.AddAsTypeOnly),
|
||||
// !!! propertyName
|
||||
}
|
||||
} else {
|
||||
debug.Assert(
|
||||
entry.namespaceLikeImport == nil || entry.namespaceLikeImport.name == symbolName,
|
||||
"Namespacelike import should be missing or match symbolName",
|
||||
)
|
||||
entry.namespaceLikeImport = &newImportBinding{
|
||||
kind: lsproto.ImportKindCommonJS,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: fix.AddAsTypeOnly,
|
||||
}
|
||||
}
|
||||
|
||||
case lsproto.ImportKindNamespace:
|
||||
debug.Assert(
|
||||
entry.namespaceLikeImport == nil || entry.namespaceLikeImport.name == symbolName,
|
||||
"Namespacelike import should be missing or match symbolName",
|
||||
)
|
||||
entry.namespaceLikeImport = &newImportBinding{
|
||||
kind: lsproto.ImportKindNamespace,
|
||||
name: symbolName,
|
||||
addAsTypeOnly: fix.AddAsTypeOnly,
|
||||
}
|
||||
}
|
||||
|
||||
case lsproto.AutoImportFixKindPromoteTypeOnly:
|
||||
// Excluding from fix-all
|
||||
default:
|
||||
debug.Fail(fmt.Sprintf("Unexpected fix kind: %v", fix.Kind))
|
||||
}
|
||||
}
|
||||
|
||||
// `NotAllowed` overrides `Required` because one addition of a new import might be required to be type-only
|
||||
// because of `--importsNotUsedAsValues=error`, but if a second addition of the same import is `NotAllowed`
|
||||
// to be type-only, the reason the first one was `Required` - the unused runtime dependency - is now moot.
|
||||
// Alternatively, if one addition is `Required` because it has no value meaning under `--preserveValueImports`
|
||||
// and `--isolatedModules`, it should be impossible for another addition to be `NotAllowed` since that would
|
||||
// mean a type is being referenced in a value location.
|
||||
func reduceAddAsTypeOnlyValues(prevValue, newValue lsproto.AddAsTypeOnly) lsproto.AddAsTypeOnly {
|
||||
if newValue > prevValue {
|
||||
return newValue
|
||||
}
|
||||
return prevValue
|
||||
}
|
||||
|
||||
func (adder *importAdder) getNewImportEntry(moduleSpecifier string, importKind lsproto.ImportKind, useRequire bool, addAsTypeOnly lsproto.AddAsTypeOnly) *importsCollection {
|
||||
// A default import that requires type-only makes the whole import type-only.
|
||||
// (We could add `default` as a named import, but that style seems undesirable.)
|
||||
// Under `--preserveValueImports` and `--importsNotUsedAsValues=error`, if a
|
||||
// module default-exports a type but named-exports some values (weird), you would
|
||||
// have to use a type-only default import and non-type-only named imports. These
|
||||
// require two separate import declarations, so we build this into the map key.
|
||||
typeOnlyKey := newImportsKey(moduleSpecifier, true /*topLevelTypeOnly*/)
|
||||
nonTypeOnlyKey := newImportsKey(moduleSpecifier, false /*topLevelTypeOnly*/)
|
||||
typeOnlyEntry := adder.newImports[typeOnlyKey]
|
||||
nonTypeOnlyEntry := adder.newImports[nonTypeOnlyKey]
|
||||
newEntry := &importsCollection{
|
||||
useRequire: useRequire,
|
||||
}
|
||||
|
||||
if importKind == lsproto.ImportKindDefault && addAsTypeOnly == lsproto.AddAsTypeOnlyRequired {
|
||||
if typeOnlyEntry != nil {
|
||||
return typeOnlyEntry
|
||||
}
|
||||
adder.newImports[typeOnlyKey] = newEntry
|
||||
return newEntry
|
||||
}
|
||||
|
||||
if addAsTypeOnly == lsproto.AddAsTypeOnlyAllowed && (typeOnlyEntry != nil || nonTypeOnlyEntry != nil) {
|
||||
if typeOnlyEntry != nil {
|
||||
return typeOnlyEntry
|
||||
}
|
||||
return nonTypeOnlyEntry
|
||||
}
|
||||
|
||||
if nonTypeOnlyEntry != nil {
|
||||
return nonTypeOnlyEntry
|
||||
}
|
||||
|
||||
adder.newImports[nonTypeOnlyKey] = newEntry
|
||||
return newEntry
|
||||
}
|
||||
|
||||
func (adder *importAdder) getAllExportsForSymbol(
|
||||
symbol *ast.Symbol,
|
||||
) []*Export {
|
||||
if export := SymbolToExport(symbol, adder.checker); export != nil {
|
||||
return adder.view.SearchByExportID(export.ExportID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TypeToAutoImportableTypeNode(
|
||||
c *checker.Checker,
|
||||
importAdder ImportAdder,
|
||||
t *checker.Type,
|
||||
contextNode *ast.Node, // !!! flags
|
||||
) *ast.TypeNode {
|
||||
idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol)
|
||||
typeNode := c.TypeToTypeNode(t, contextNode, nodebuilder.FlagsNone, idToSymbol)
|
||||
if typeNode == nil {
|
||||
return nil
|
||||
}
|
||||
return TypeNodeToAutoImportableTypeNode(typeNode, importAdder, idToSymbol)
|
||||
}
|
||||
|
||||
// TypeNodeToAutoImportableTypeNode converts import type references in a type node to
|
||||
// simple type references and registers needed imports with the import adder.
|
||||
func TypeNodeToAutoImportableTypeNode(
|
||||
typeNode *ast.TypeNode,
|
||||
importAdder ImportAdder,
|
||||
idToSymbol map[*ast.IdentifierNode]*ast.Symbol,
|
||||
) *ast.TypeNode {
|
||||
referenceTypeNode, importableSymbols := TryGetAutoImportableReferenceFromTypeNode(typeNode, idToSymbol)
|
||||
if referenceTypeNode != nil {
|
||||
if importAdder != nil {
|
||||
importSymbols(importAdder, importableSymbols)
|
||||
}
|
||||
typeNode = referenceTypeNode
|
||||
}
|
||||
|
||||
// !!! handle type node reuse: nodes needs to be fresh here but also preserve symbols
|
||||
return typeNode
|
||||
}
|
||||
|
||||
func importSymbols(importAdder ImportAdder, symbols []*ast.Symbol) {
|
||||
for _, symbol := range symbols {
|
||||
importAdder.AddImportFromExportedSymbol(symbol, true /*isValidTypeOnlyUseSite*/)
|
||||
}
|
||||
}
|
||||
|
||||
// Given a type node containing 'import("./a").SomeType<import("./b").OtherType<...>>',
|
||||
// returns an equivalent type reference node with any nested ImportTypeNodes also replaced
|
||||
// with type references, and a list of symbols that must be imported to use the type reference.
|
||||
// TryGetAutoImportableReferenceFromTypeNode converts import type references in a type node
|
||||
// to simple type references and returns the transformed type node and the symbols that need
|
||||
// to be imported.
|
||||
func TryGetAutoImportableReferenceFromTypeNode(importTypeNode *ast.TypeNode, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) (*ast.TypeNode, []*ast.Symbol) {
|
||||
var symbols []*ast.Symbol
|
||||
var visitor *ast.NodeVisitor
|
||||
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
|
||||
visit := func(node *ast.Node) *ast.Node {
|
||||
if ast.IsLiteralImportTypeNode(node) && node.AsImportTypeNode().Qualifier != nil {
|
||||
importTypeNode := node.AsImportTypeNode()
|
||||
// Symbol for the left-most thing after the dot
|
||||
firstIdentifier := ast.GetFirstIdentifier(importTypeNode.Qualifier)
|
||||
symbol := idToSymbol[firstIdentifier]
|
||||
if symbol == nil {
|
||||
// if symbol is missing then this doesn't come from a synthesized import type node
|
||||
// it has to be an import type node authored by the user and thus it has to be valid
|
||||
// it can't refer to reserved internal symbol names and such
|
||||
return node.VisitEachChild(visitor)
|
||||
}
|
||||
name := getNameForExportedSymbol(symbol, false /*preferCapitalized*/)
|
||||
var qualifier *ast.EntityName
|
||||
if name != firstIdentifier.Text() {
|
||||
qualifier = replaceFirstIdentifierOfEntityName(factory, importTypeNode.Qualifier, factory.NewIdentifier(name))
|
||||
} else {
|
||||
qualifier = importTypeNode.Qualifier
|
||||
}
|
||||
symbols = append(symbols, symbol)
|
||||
typeArguments := visitor.VisitNodes(importTypeNode.TypeArguments)
|
||||
return factory.NewTypeReferenceNode(qualifier, typeArguments)
|
||||
}
|
||||
return visitor.VisitEachChild(node)
|
||||
}
|
||||
visitor = ast.NewNodeVisitor(visit, factory, ast.NodeVisitorHooks{})
|
||||
|
||||
typeNode := visitor.VisitNode(importTypeNode)
|
||||
debug.Assert(typeNode == nil || ast.IsTypeNode(typeNode), "expected a type node")
|
||||
return typeNode, symbols
|
||||
}
|
||||
|
||||
// If a type checker and multiple files are available, consider using `forEachNameOfDefaultExport`
|
||||
// instead, which searches for names of re-exported defaults/namespaces in target files.
|
||||
func getNameForExportedSymbol(symbol *ast.Symbol, preferCapitalized bool) string {
|
||||
if symbol.Name == ast.InternalSymbolNameExportEquals || symbol.Name == ast.InternalSymbolNameDefault {
|
||||
// Names for default exports:
|
||||
// - export default foo => foo
|
||||
// - export { foo as default } => foo
|
||||
// - export default 0 => filename converted to camelCase
|
||||
name := getDefaultLikeExportNameFromDeclaration(symbol)
|
||||
if name != "" {
|
||||
return name
|
||||
}
|
||||
debug.Assert(symbol.Parent != nil, "Expected exported symbol to have module symbol as parent")
|
||||
return lsutil.ModuleSymbolToValidIdentifier(symbol.Parent, preferCapitalized)
|
||||
}
|
||||
return symbol.Name
|
||||
}
|
||||
|
||||
func replaceFirstIdentifierOfEntityName(factory *ast.NodeFactory, name *ast.EntityName, newIdentifier *ast.IdentifierNode) *ast.EntityName {
|
||||
if name.Kind == ast.KindIdentifier {
|
||||
return newIdentifier
|
||||
}
|
||||
return factory.NewQualifiedName(
|
||||
replaceFirstIdentifierOfEntityName(factory, name.AsQualifiedName().Left, newIdentifier),
|
||||
name.AsQualifiedName().Right,
|
||||
)
|
||||
}
|
||||
|
||||
func (adder *importAdder) getImportFixForSymbol(view *View, file *ast.SourceFile, exports []*Export, isValidTypeOnlyUseSite bool) *Fix {
|
||||
fixes := core.FlatMap(exports, func(export *Export) []*Fix {
|
||||
return view.GetFixes(adder.ctx, export, false /*forJSX*/, isValidTypeOnlyUseSite, nil /*usagePosition*/)
|
||||
})
|
||||
slices.SortFunc(fixes, func(a, b *Fix) int {
|
||||
return view.CompareFixesForRanking(a, b)
|
||||
})
|
||||
if len(fixes) > 0 {
|
||||
return fixes[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
186
tools/tsgo/internal/ls/autoimport/index.go
Normal file
186
tools/tsgo/internal/ls/autoimport/index.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
)
|
||||
|
||||
// Named is a constraint for types that can provide their name.
|
||||
type Named interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
// Index stores entries with an index mapping uppercase letters to entries whose name
|
||||
// starts with that letter, and lowercase letters to entries whose name contains a
|
||||
// word starting with that letter.
|
||||
type Index[T Named] struct {
|
||||
entries []T
|
||||
index map[rune][]int
|
||||
}
|
||||
|
||||
func (idx *Index[T]) Find(name string, caseSensitive bool) []T {
|
||||
if len(idx.entries) == 0 || len(name) == 0 {
|
||||
return nil
|
||||
}
|
||||
firstRune := core.FirstResult(utf8.DecodeRuneInString(name))
|
||||
if firstRune == utf8.RuneError {
|
||||
return nil
|
||||
}
|
||||
firstRuneUpper := unicode.ToUpper(firstRune)
|
||||
candidates, ok := idx.index[firstRuneUpper]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var results []T
|
||||
for _, entryIndex := range candidates {
|
||||
entry := idx.entries[entryIndex]
|
||||
entryName := entry.Name()
|
||||
if (caseSensitive && entryName == name) || (!caseSensitive && strings.EqualFold(entryName, name)) {
|
||||
results = append(results, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// SearchWordPrefix returns each entry whose name contains a word beginning with
|
||||
// the first character of 'prefix', and whose name contains all characters
|
||||
// of 'prefix' in order (case-insensitive). If 'filter' is provided, only entries
|
||||
// for which filter(entry) returns true are included.
|
||||
func (idx *Index[T]) SearchWordPrefix(prefix string) []T {
|
||||
if len(idx.entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(prefix) == 0 {
|
||||
return idx.entries
|
||||
}
|
||||
|
||||
prefix = strings.ToLower(prefix)
|
||||
firstRune, _ := utf8.DecodeRuneInString(prefix)
|
||||
if firstRune == utf8.RuneError {
|
||||
return nil
|
||||
}
|
||||
|
||||
firstRuneUpper := unicode.ToUpper(firstRune)
|
||||
firstRuneLower := unicode.ToLower(firstRune)
|
||||
|
||||
// Look up entries that have words starting with this letter
|
||||
var wordStarts []int
|
||||
nameStarts, _ := idx.index[firstRuneUpper]
|
||||
if firstRuneUpper != firstRuneLower {
|
||||
wordStarts, _ = idx.index[firstRuneLower]
|
||||
}
|
||||
count := len(nameStarts) + len(wordStarts)
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter entries by checking if they contain all characters in order
|
||||
results := make([]T, 0, count)
|
||||
for _, starts := range [][]int{nameStarts, wordStarts} {
|
||||
for _, i := range starts {
|
||||
entry := idx.entries[i]
|
||||
if containsCharsInOrder(entry.Name(), prefix) {
|
||||
results = append(results, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// containsCharsInOrder checks if str contains all characters from pattern in order (case-insensitive).
|
||||
func containsCharsInOrder(str, pattern string) bool {
|
||||
str = strings.ToLower(str)
|
||||
pattern = strings.ToLower(pattern)
|
||||
|
||||
patternIdx := 0
|
||||
for _, ch := range str {
|
||||
if patternIdx < len(pattern) {
|
||||
patternRune, size := utf8.DecodeRuneInString(pattern[patternIdx:])
|
||||
if ch == patternRune {
|
||||
patternIdx += size
|
||||
}
|
||||
}
|
||||
}
|
||||
return patternIdx == len(pattern)
|
||||
}
|
||||
|
||||
// insertAsWords adds a value to the index keyed by the first letter of each word in its name.
|
||||
func (idx *Index[T]) insertAsWords(value T) {
|
||||
if idx.index == nil {
|
||||
idx.index = make(map[rune][]int)
|
||||
}
|
||||
|
||||
name := value.Name()
|
||||
if len(name) == 0 {
|
||||
panic("Cannot index entry with empty name")
|
||||
}
|
||||
entryIndex := len(idx.entries)
|
||||
idx.entries = append(idx.entries, value)
|
||||
|
||||
indices := wordIndices(name)
|
||||
seenRunes := make(map[rune]bool)
|
||||
|
||||
for i, start := range indices {
|
||||
substr := name[start:]
|
||||
firstRune, _ := utf8.DecodeRuneInString(substr)
|
||||
if firstRune == utf8.RuneError {
|
||||
continue
|
||||
}
|
||||
if i == 0 {
|
||||
// Name start keyed by uppercase
|
||||
firstRune = unicode.ToUpper(firstRune)
|
||||
idx.index[firstRune] = append(idx.index[firstRune], entryIndex)
|
||||
seenRunes[firstRune] = true // (Still set seenRunes in case first character is non-alphabetic)
|
||||
} else {
|
||||
// Subsequent word starts keyed by lowercase
|
||||
firstRune = unicode.ToLower(firstRune)
|
||||
if !seenRunes[firstRune] {
|
||||
idx.index[firstRune] = append(idx.index[firstRune], entryIndex)
|
||||
seenRunes[firstRune] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clone creates a new Index containing only entries for which filter returns true.
|
||||
func (idx *Index[T]) Clone(filter func(T) bool) *Index[T] {
|
||||
if idx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newIdx := &Index[T]{
|
||||
entries: make([]T, 0, len(idx.entries)),
|
||||
index: make(map[rune][]int, len(idx.index)),
|
||||
}
|
||||
|
||||
// Build mapping from old index to new index for filtered entries
|
||||
oldToNew := make(map[int]int, len(idx.entries))
|
||||
for oldIndex, entry := range idx.entries {
|
||||
if filter(entry) {
|
||||
newIndex := len(newIdx.entries)
|
||||
newIdx.entries = append(newIdx.entries, entry)
|
||||
oldToNew[oldIndex] = newIndex
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the index with remapped indices
|
||||
for r, oldIndices := range idx.index {
|
||||
newIndices := make([]int, 0, len(oldIndices))
|
||||
for _, oldIndex := range oldIndices {
|
||||
if newIndex, ok := oldToNew[oldIndex]; ok {
|
||||
newIndices = append(newIndices, newIndex)
|
||||
}
|
||||
}
|
||||
if len(newIndices) > 0 {
|
||||
newIdx.index[r] = newIndices
|
||||
}
|
||||
}
|
||||
|
||||
return newIdx
|
||||
}
|
||||
79
tools/tsgo/internal/ls/autoimport/index_test.go
Normal file
79
tools/tsgo/internal/ls/autoimport/index_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
type testEntry struct {
|
||||
name string
|
||||
package_ string
|
||||
}
|
||||
|
||||
func (e *testEntry) Name() string { return e.name }
|
||||
|
||||
func TestIndexClone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("filters entries by package", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
idx := &Index[*testEntry]{}
|
||||
idx.insertAsWords(&testEntry{name: "fooBar", package_: "pkg-a"})
|
||||
idx.insertAsWords(&testEntry{name: "bazQux", package_: "pkg-b"})
|
||||
idx.insertAsWords(&testEntry{name: "fooQux", package_: "pkg-a"})
|
||||
|
||||
// Clone excluding pkg-b
|
||||
cloned := idx.Clone(func(e *testEntry) bool {
|
||||
return e.package_ != "pkg-b"
|
||||
})
|
||||
|
||||
// Original should have all 3 entries
|
||||
assert.Equal(t, len(idx.entries), 3)
|
||||
|
||||
// Cloned should have 2 entries (only pkg-a)
|
||||
assert.Equal(t, len(cloned.entries), 2)
|
||||
|
||||
// Search should work on cloned index
|
||||
results := cloned.Find("fooBar", true)
|
||||
assert.Equal(t, len(results), 1)
|
||||
assert.Equal(t, results[0].name, "fooBar")
|
||||
|
||||
// bazQux should not be in cloned index
|
||||
results = cloned.Find("bazQux", true)
|
||||
assert.Equal(t, len(results), 0)
|
||||
|
||||
// Word prefix search should work
|
||||
results = cloned.SearchWordPrefix("foo")
|
||||
assert.Equal(t, len(results), 2)
|
||||
})
|
||||
|
||||
t.Run("handles nil index", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var idx *Index[*testEntry]
|
||||
cloned := idx.Clone(func(e *testEntry) bool { return true })
|
||||
assert.Assert(t, cloned == nil)
|
||||
})
|
||||
|
||||
t.Run("handles empty index", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
idx := &Index[*testEntry]{}
|
||||
cloned := idx.Clone(func(e *testEntry) bool { return true })
|
||||
assert.Equal(t, len(cloned.entries), 0)
|
||||
})
|
||||
|
||||
t.Run("filters all entries", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
idx := &Index[*testEntry]{}
|
||||
idx.insertAsWords(&testEntry{name: "fooBar", package_: "pkg-a"})
|
||||
idx.insertAsWords(&testEntry{name: "bazQux", package_: "pkg-b"})
|
||||
|
||||
cloned := idx.Clone(func(e *testEntry) bool { return false })
|
||||
assert.Equal(t, len(cloned.entries), 0)
|
||||
assert.Equal(t, len(cloned.index), 0)
|
||||
})
|
||||
}
|
||||
1826
tools/tsgo/internal/ls/autoimport/registry.go
Normal file
1826
tools/tsgo/internal/ls/autoimport/registry.go
Normal file
File diff suppressed because it is too large
Load Diff
1183
tools/tsgo/internal/ls/autoimport/registry_test.go
Normal file
1183
tools/tsgo/internal/ls/autoimport/registry_test.go
Normal file
File diff suppressed because it is too large
Load Diff
75
tools/tsgo/internal/ls/autoimport/specifiers.go
Normal file
75
tools/tsgo/internal/ls/autoimport/specifiers.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/modulespecifiers"
|
||||
)
|
||||
|
||||
func (v *View) GetModuleSpecifier(
|
||||
export *Export,
|
||||
userPreferences modulespecifiers.UserPreferences,
|
||||
) (string, modulespecifiers.ResultKind) {
|
||||
// Ambient module
|
||||
if modulespecifiers.PathIsBareSpecifier(string(export.ModuleID)) {
|
||||
specifier := string(export.ModuleID)
|
||||
if modulespecifiers.IsExcludedByRegex(specifier, userPreferences.AutoImportSpecifierExcludeRegexes) {
|
||||
return "", modulespecifiers.ResultKindNone
|
||||
}
|
||||
return string(export.ModuleID), modulespecifiers.ResultKindAmbient
|
||||
}
|
||||
|
||||
if export.PackageName != "" {
|
||||
if entrypoints, ok := v.registry.entrypoints[export.Path]; ok {
|
||||
for _, entrypoint := range entrypoints {
|
||||
if entrypoint.IncludeConditions.IsSubsetOf(v.conditions) && !v.conditions.Intersects(entrypoint.ExcludeConditions) {
|
||||
specifier := modulespecifiers.ProcessEntrypointEnding(
|
||||
entrypoint,
|
||||
userPreferences,
|
||||
v.program,
|
||||
v.program.Options(),
|
||||
v.importingFile,
|
||||
v.getAllowedEndings(),
|
||||
)
|
||||
|
||||
if !modulespecifiers.IsExcludedByRegex(specifier, userPreferences.AutoImportSpecifierExcludeRegexes) {
|
||||
return specifier, modulespecifiers.ResultKindNodeModules
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", modulespecifiers.ResultKindNone
|
||||
}
|
||||
}
|
||||
|
||||
cache := v.registry.specifierCache[v.importingFile.Path()]
|
||||
if export.PackageName == "" {
|
||||
if specifier, ok := cache.Load(export.Path); ok {
|
||||
if specifier == "" {
|
||||
return "", modulespecifiers.ResultKindNone
|
||||
}
|
||||
return specifier, modulespecifiers.ResultKindRelative
|
||||
}
|
||||
}
|
||||
|
||||
specifiers, kind := modulespecifiers.GetModuleSpecifiersForFileWithInfo(
|
||||
v.importingFile,
|
||||
export.ModuleFileName,
|
||||
v.program.Options(),
|
||||
v.program,
|
||||
userPreferences,
|
||||
modulespecifiers.ModuleSpecifierOptions{},
|
||||
true,
|
||||
)
|
||||
// !!! unsure when this could return multiple specifiers combined with the
|
||||
// new node_modules code. Possibly with local symlinks, which should be
|
||||
// very rare.
|
||||
for _, specifier := range specifiers {
|
||||
if strings.Contains(specifier, "/node_modules/") {
|
||||
continue
|
||||
}
|
||||
cache.Store(export.Path, specifier)
|
||||
return specifier, kind
|
||||
}
|
||||
cache.Store(export.Path, "")
|
||||
return "", modulespecifiers.ResultKindNone
|
||||
}
|
||||
14
tools/tsgo/internal/ls/autoimport/testmain_test.go
Normal file
14
tools/tsgo/internal/ls/autoimport/testmain_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package autoimport_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/testutil/baseline"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
core.ApplyDebugStackLimit()
|
||||
defer baseline.Track()()
|
||||
m.Run()
|
||||
}
|
||||
323
tools/tsgo/internal/ls/autoimport/util.go
Normal file
323
tools/tsgo/internal/ls/autoimport/util.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"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/module"
|
||||
"github.com/microsoft/typescript-go/internal/modulespecifiers"
|
||||
"github.com/microsoft/typescript-go/internal/packagejson"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/wrapvfs"
|
||||
)
|
||||
|
||||
func tryGetModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, string, bool) {
|
||||
if !symbol.IsExternalModule() {
|
||||
return "", "", false
|
||||
}
|
||||
decl := ast.GetNonAugmentationDeclaration(symbol)
|
||||
if decl == nil {
|
||||
return "", "", false
|
||||
}
|
||||
if decl.Kind == ast.KindSourceFile {
|
||||
return ModuleID(decl.AsSourceFile().Path()), decl.AsSourceFile().FileName(), true
|
||||
}
|
||||
if ast.IsModuleWithStringLiteralName(decl) {
|
||||
return ModuleID(decl.Name().Text()), "", true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func getModuleIDAndFileNameOfModuleSymbol(symbol *ast.Symbol) (ModuleID, string) {
|
||||
if !symbol.IsExternalModule() {
|
||||
panic("symbol is not an external module")
|
||||
}
|
||||
decl := ast.GetNonAugmentationDeclaration(symbol)
|
||||
if decl == nil {
|
||||
panic("module symbol has no non-augmentation declaration")
|
||||
}
|
||||
if decl.Kind == ast.KindSourceFile {
|
||||
return ModuleID(decl.AsSourceFile().Path()), decl.AsSourceFile().FileName()
|
||||
}
|
||||
if ast.IsModuleWithStringLiteralName(decl) {
|
||||
return ModuleID(decl.Name().Text()), ""
|
||||
}
|
||||
panic("could not determine module ID of module symbol")
|
||||
}
|
||||
|
||||
// wordIndices splits an identifier into its constituent words based on camelCase and snake_case conventions
|
||||
// by returning the starting byte indices of each word. The first index is always 0.
|
||||
// - CamelCase
|
||||
// ^ ^
|
||||
// - snake_case
|
||||
// ^ ^
|
||||
// - ParseURL
|
||||
// ^ ^
|
||||
// - __proto__
|
||||
// ^
|
||||
func wordIndices(s string) []int {
|
||||
var indices []int
|
||||
for byteIndex, runeValue := range s {
|
||||
if byteIndex == 0 {
|
||||
indices = append(indices, byteIndex)
|
||||
continue
|
||||
}
|
||||
if runeValue == '_' {
|
||||
if byteIndex+1 < len(s) && s[byteIndex+1] != '_' {
|
||||
indices = append(indices, byteIndex+1)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if unicode.IsUpper(runeValue) && (unicode.IsLower(core.FirstResult(utf8.DecodeLastRuneInString(s[:byteIndex]))) || (byteIndex+1 < len(s) && unicode.IsLower(core.FirstResult(utf8.DecodeRuneInString(s[byteIndex+1:]))))) {
|
||||
indices = append(indices, byteIndex)
|
||||
}
|
||||
}
|
||||
return indices
|
||||
}
|
||||
|
||||
func getPackageNamesInNodeModules(nodeModulesDir string, fs vfs.FS) *collections.Set[string] {
|
||||
packageNames := &collections.Set[string]{}
|
||||
if tspath.GetBaseFileName(nodeModulesDir) != "node_modules" {
|
||||
panic("nodeModulesDir is not a node_modules directory")
|
||||
}
|
||||
// A missing node_modules directory yields no entries (GetAccessibleEntries returns
|
||||
// empty), so there's no need to check existence first: a deleted node_modules is
|
||||
// handled upstream in updateBucketAndDirectoryExistence, which drops the bucket.
|
||||
entries := fs.GetAccessibleEntries(nodeModulesDir)
|
||||
for _, baseName := range entries.Directories {
|
||||
if baseName[0] == '.' {
|
||||
continue
|
||||
}
|
||||
if baseName[0] == '@' {
|
||||
scopedDirPath := tspath.CombinePaths(nodeModulesDir, baseName)
|
||||
for _, scopedPackageDirName := range fs.GetAccessibleEntries(scopedDirPath).Directories {
|
||||
scopedBaseName := tspath.GetBaseFileName(scopedPackageDirName)
|
||||
if baseName == "@types" {
|
||||
packageNames.Add(module.GetPackageNameFromTypesPackageName(tspath.CombinePaths("@types", scopedBaseName)))
|
||||
} else {
|
||||
packageNames.Add(tspath.CombinePaths(baseName, scopedBaseName))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
packageNames.Add(baseName)
|
||||
}
|
||||
return packageNames
|
||||
}
|
||||
|
||||
func getDefaultLikeExportNameFromDeclaration(symbol *ast.Symbol) string {
|
||||
for _, d := range symbol.Declarations {
|
||||
// "export default" in this case. See `ExportAssignment`for more details.
|
||||
if ast.IsExportAssignment(d) {
|
||||
if innerExpression := ast.SkipOuterExpressions(d.Expression(), ast.OEKAll); ast.IsIdentifier(innerExpression) {
|
||||
return innerExpression.Text()
|
||||
}
|
||||
continue
|
||||
}
|
||||
// "export { ~ as default }"
|
||||
if ast.IsExportSpecifier(d) && d.Symbol().Flags == ast.SymbolFlagsAlias && d.PropertyName() != nil {
|
||||
if d.PropertyName().Kind == ast.KindIdentifier {
|
||||
return d.PropertyName().Text()
|
||||
}
|
||||
continue
|
||||
}
|
||||
// GH#52694
|
||||
if name := ast.GetNameOfDeclaration(d); name != nil && name.Kind == ast.KindIdentifier {
|
||||
return name.Text()
|
||||
}
|
||||
if symbol.Parent != nil && !checker.IsExternalModuleSymbol(symbol.Parent) {
|
||||
return symbol.Parent.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getResolvedPackageNames(ctx context.Context, program *compiler.Program) *collections.Set[string] {
|
||||
rawNames := program.ResolvedPackageNames()
|
||||
unresolvedPackageNames := program.UnresolvedPackageNames()
|
||||
|
||||
// Normalize @types/ package names to their actual package names
|
||||
// (e.g., "@types/react" → "react"). ResolvedPackageNames can contain
|
||||
// @types names when the program resolves an import like "react" to
|
||||
// "@types/react/index.d.ts" via the PackageId.Name field.
|
||||
resolvedPackageNames := collections.NewSetWithSizeHint[string](rawNames.Len())
|
||||
for name := range rawNames.Keys() {
|
||||
resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(name))
|
||||
}
|
||||
|
||||
for _, name := range program.Options().Types {
|
||||
if name != "*" {
|
||||
resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(name))
|
||||
}
|
||||
}
|
||||
|
||||
if unresolvedPackageNames.Len() > 0 {
|
||||
checker, done := program.GetTypeChecker(ctx)
|
||||
defer done()
|
||||
for name := range unresolvedPackageNames.Keys() {
|
||||
if symbol := checker.TryFindAmbientModule(name); symbol != nil {
|
||||
declaringFile := ast.GetSourceFileOfModule(symbol)
|
||||
if packageName := modulespecifiers.GetPackageNameFromDirectory(declaringFile.FileName()); packageName != "" {
|
||||
resolvedPackageNames.Add(module.GetPackageNameFromTypesPackageName(packageName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolvedPackageNames
|
||||
}
|
||||
|
||||
// addProjectReferenceOutputMappings adds output .d.ts to source file mappings
|
||||
// from a program's project references to the provided map.
|
||||
// This is used during node_modules bucket building to redirect extraction
|
||||
// from output files to source files when the output is from a project reference.
|
||||
func addProjectReferenceOutputMappings(program *compiler.Program, result map[tspath.Path]string) {
|
||||
refs := program.GetResolvedProjectReferences()
|
||||
for _, ref := range refs {
|
||||
if ref == nil {
|
||||
continue
|
||||
}
|
||||
ref.ParseInputOutputNames()
|
||||
for outputDtsPath, mapping := range ref.OutputDtsToProjectReference() {
|
||||
// Only add if not already present (first program wins)
|
||||
if _, exists := result[outputDtsPath]; !exists {
|
||||
result[outputDtsPath] = mapping.Source
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createCheckerPool(program checker.Program) (getChecker func() (*checker.Checker, func()), closePool func(), getCreatedCount func() int32) {
|
||||
maxSize := int32(runtime.GOMAXPROCS(0))
|
||||
pool := make(chan *checker.Checker, maxSize)
|
||||
var created atomic.Int32
|
||||
|
||||
return func() (*checker.Checker, func()) {
|
||||
// Try to get an existing checker
|
||||
select {
|
||||
case ch := <-pool:
|
||||
return ch, func() { pool <- ch }
|
||||
default:
|
||||
break
|
||||
}
|
||||
// Try to create a new one if under limit
|
||||
for {
|
||||
current := created.Load()
|
||||
if current >= maxSize {
|
||||
// At limit, wait for one to become available
|
||||
ch := <-pool
|
||||
return ch, func() { pool <- ch }
|
||||
}
|
||||
if created.CompareAndSwap(current, current+1) {
|
||||
ch := core.FirstResult(checker.NewChecker(program, nil))
|
||||
return ch, func() { pool <- ch }
|
||||
}
|
||||
}
|
||||
}, func() {
|
||||
close(pool)
|
||||
}, func() int32 {
|
||||
return created.Load()
|
||||
}
|
||||
}
|
||||
|
||||
// addPackageJsonDependencies adds all dependencies and peerDependencies from a package.json
|
||||
// to the given set, canonicalizing @types package names to their base names.
|
||||
func addPackageJsonDependencies(contents *packagejson.PackageJson, deps *collections.Set[string]) {
|
||||
contents.RangeDependencies(func(name, _, field string) bool {
|
||||
if name == "" || name == "@types/" || name[0] == '.' {
|
||||
// Edge cases that could make us blow up probably
|
||||
return true
|
||||
}
|
||||
if field == "dependencies" || field == "peerDependencies" {
|
||||
deps.Add(module.GetPackageNameFromTypesPackageName(name))
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// getPackageRealpathFuncs returns functions to transform between symlink and realpath for files within a package.
|
||||
// It calls FS.Realpath once per package directory and uses prefix substitution for files within that directory,
|
||||
// avoiding expensive realpath syscalls for each file. For files outside the package (e.g. re-exported
|
||||
// dependencies reached through node_modules symlinks), it resolves the file's directory realpath once,
|
||||
// finds the symlink boundary (the package root where the symlink lives), and caches that prefix mapping.
|
||||
// All subsequent files under the same symlinked package directory use prefix substitution with no syscalls.
|
||||
func getPackageRealpathFuncs(fs vfs.FS, packageDir string) (toRealpath, toSymlink func(string) string) {
|
||||
realPackageDir := fs.Realpath(packageDir)
|
||||
isSymlinked := realPackageDir != packageDir
|
||||
// Cache of package-directory-level symlink→realpath prefix mappings for
|
||||
// external packages encountered via re-exports. Keyed by the node_modules
|
||||
// package directory (e.g. "/app/node_modules/dep"), so all files under
|
||||
// that package reuse a single realpath lookup.
|
||||
dirCache := make(map[string]string)
|
||||
toRealpath = func(fileName string) string {
|
||||
// Fast path: files within the package use prefix substitution.
|
||||
if isSymlinked {
|
||||
if after, ok := strings.CutPrefix(fileName, packageDir); ok {
|
||||
return realPackageDir + after
|
||||
}
|
||||
}
|
||||
// Files outside the package (e.g. re-exports into symlinked deps):
|
||||
// find the node_modules package directory, resolve it once, and cache.
|
||||
pkgDir := module.ParseNodeModuleFromPath(fileName, false /*isFolder*/)
|
||||
if pkgDir == "" {
|
||||
return fileName
|
||||
}
|
||||
if realDir, ok := dirCache[pkgDir]; ok {
|
||||
if realDir == pkgDir {
|
||||
return fileName
|
||||
}
|
||||
return realDir + fileName[len(pkgDir):]
|
||||
}
|
||||
realDir := fs.Realpath(pkgDir)
|
||||
dirCache[pkgDir] = realDir
|
||||
if realDir == pkgDir {
|
||||
return fileName
|
||||
}
|
||||
return realDir + fileName[len(pkgDir):]
|
||||
}
|
||||
if !isSymlinked {
|
||||
return toRealpath, core.Identity
|
||||
}
|
||||
// toSymlink only handles files within the package directory (reversing the
|
||||
// packageDir→realPackageDir substitution). It does not handle arbitrary external
|
||||
// paths; callers should only use it for files known to be within the package.
|
||||
toSymlink = func(fileName string) string {
|
||||
if after, ok := strings.CutPrefix(fileName, realPackageDir); ok {
|
||||
return packageDir + after
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
return toRealpath, toSymlink
|
||||
}
|
||||
|
||||
type resolutionHost struct {
|
||||
fs vfs.FS
|
||||
currentDirectory string
|
||||
}
|
||||
|
||||
var _ module.ResolutionHost = (*resolutionHost)(nil)
|
||||
|
||||
func (rh *resolutionHost) GetCurrentDirectory() string {
|
||||
return rh.currentDirectory
|
||||
}
|
||||
|
||||
func (rh *resolutionHost) FS() vfs.FS {
|
||||
return rh.fs
|
||||
}
|
||||
|
||||
func getModuleResolver(host RegistryCloneHost, realpath func(string) string, opts module.ResolverOptions) *module.Resolver {
|
||||
rh := &resolutionHost{
|
||||
fs: wrapvfs.Wrap(host.FS(), wrapvfs.Replacements{Realpath: realpath}),
|
||||
currentDirectory: host.GetCurrentDirectory(),
|
||||
}
|
||||
return module.NewResolverWithOptions(rh, core.EmptyCompilerOptions, "", "", opts)
|
||||
}
|
||||
220
tools/tsgo/internal/ls/autoimport/util_test.go
Normal file
220
tools/tsgo/internal/ls/autoimport/util_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package autoimport
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestWordIndices(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
input string
|
||||
expectedWords []string
|
||||
}{
|
||||
// Basic camelCase
|
||||
{
|
||||
input: "camelCase",
|
||||
expectedWords: []string{"camelCase", "Case"},
|
||||
},
|
||||
// snake_case
|
||||
{
|
||||
input: "snake_case",
|
||||
expectedWords: []string{"snake_case", "case"},
|
||||
},
|
||||
// ParseURL - uppercase sequence followed by lowercase
|
||||
{
|
||||
input: "ParseURL",
|
||||
expectedWords: []string{"ParseURL", "URL"},
|
||||
},
|
||||
// XMLHttpRequest - multiple uppercase sequences
|
||||
{
|
||||
input: "XMLHttpRequest",
|
||||
expectedWords: []string{"XMLHttpRequest", "HttpRequest", "Request"},
|
||||
},
|
||||
// Single word lowercase
|
||||
{
|
||||
input: "hello",
|
||||
expectedWords: []string{"hello"},
|
||||
},
|
||||
// Single word uppercase
|
||||
{
|
||||
input: "HELLO",
|
||||
expectedWords: []string{"HELLO"},
|
||||
},
|
||||
// Mixed with numbers
|
||||
{
|
||||
input: "parseHTML5Parser",
|
||||
expectedWords: []string{"parseHTML5Parser", "HTML5Parser", "Parser"},
|
||||
},
|
||||
// Underscore variations
|
||||
{
|
||||
input: "__proto__",
|
||||
expectedWords: []string{"__proto__", "proto__"},
|
||||
},
|
||||
{
|
||||
input: "_private_member",
|
||||
expectedWords: []string{"_private_member", "member"},
|
||||
},
|
||||
// Single character
|
||||
{
|
||||
input: "a",
|
||||
expectedWords: []string{"a"},
|
||||
},
|
||||
{
|
||||
input: "A",
|
||||
expectedWords: []string{"A"},
|
||||
},
|
||||
// Consecutive underscores
|
||||
{
|
||||
input: "test__double__underscore",
|
||||
expectedWords: []string{"test__double__underscore", "double__underscore", "underscore"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
indices := wordIndices(tt.input)
|
||||
|
||||
// Convert indices to actual word slices for comparison
|
||||
var actualWords []string
|
||||
for _, idx := range indices {
|
||||
actualWords = append(actualWords, tt.input[idx:])
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actualWords, tt.expectedWords) {
|
||||
t.Errorf("wordIndices(%q) produced words %v, want %v", tt.input, actualWords, tt.expectedWords)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPackageRealpathFuncs_FollowsNodeModulesSymlinks tests that toRealpath correctly
|
||||
// follows symlinks for files outside the package directory (e.g. node_modules entries).
|
||||
// Without this, the module resolver uses unresolved symlink paths as cache keys, causing
|
||||
// the same file to be loaded multiple times and triggering massive memory usage when
|
||||
// barrel files re-export from many symlinked packages (issue #2780).
|
||||
func TestGetPackageRealpathFuncs_FollowsNodeModulesSymlinks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Simulate a layout where the package directory is itself a symlink (e.g. Bazel's
|
||||
// convenience symlinks or pnpm's virtual store):
|
||||
// /symlink-bin/pkg/ -> symlink to /real/bin/pkg/
|
||||
// /real/bin/pkg/node_modules/dep -> symlink to /real/dep/
|
||||
//
|
||||
// When toRealpath is used as the module resolver's Realpath, it must follow
|
||||
// the node_modules symlink so that /real/bin/pkg/node_modules/dep/index.d.ts
|
||||
// resolves to /real/dep/index.d.ts — otherwise the same dep file gets different
|
||||
// cache keys depending on which path it was reached through.
|
||||
fs := vfstest.FromMap(map[string]any{
|
||||
"/symlink-bin/pkg": vfstest.Symlink("/real/bin/pkg"),
|
||||
"/real/bin/pkg/index.d.ts": "export declare const a: number;",
|
||||
"/real/bin/pkg/node_modules/dep": vfstest.Symlink("/real/dep"),
|
||||
"/real/dep/index.d.ts": "export declare const b: number;",
|
||||
"/real/dep/src/utils/helper.d.ts": "export declare const c: number;",
|
||||
}, true)
|
||||
|
||||
toRealpath, _ := getPackageRealpathFuncs(fs, "/symlink-bin/pkg")
|
||||
|
||||
// Files inside the package should be converted via string replacement (fast path).
|
||||
assert.Equal(
|
||||
t,
|
||||
toRealpath("/symlink-bin/pkg/index.d.ts"),
|
||||
"/real/bin/pkg/index.d.ts",
|
||||
"package files should be converted via prefix replacement",
|
||||
)
|
||||
|
||||
// Files outside the package (e.g. node_modules symlinks) should be resolved via
|
||||
// fs.Realpath so the cache key is the canonical realpath, not the symlink path.
|
||||
assert.Equal(
|
||||
t,
|
||||
toRealpath("/real/bin/pkg/node_modules/dep/index.d.ts"),
|
||||
"/real/dep/index.d.ts",
|
||||
"node_modules symlinks must be followed so the same file gets a consistent cache key",
|
||||
)
|
||||
|
||||
// Files in subdirectories of an already-resolved external package should
|
||||
// use the cached prefix mapping without additional realpath calls.
|
||||
assert.Equal(
|
||||
t,
|
||||
toRealpath("/real/bin/pkg/node_modules/dep/src/utils/helper.d.ts"),
|
||||
"/real/dep/src/utils/helper.d.ts",
|
||||
"subdirectories of a resolved external package should use cached prefix mapping",
|
||||
)
|
||||
}
|
||||
|
||||
// TestGetPackageRealpathFuncs_DuplicateCacheKeys demonstrates how the broken toRealpath
|
||||
// causes the same physical file to get different cache keys when reached through different
|
||||
// symlink paths. In pnpm/Bazel monorepos, multiple packages may have node_modules symlinks
|
||||
// that point to the same physical dependency. Because toRealpath doesn't follow symlinks
|
||||
// for files outside the package directory, each path is treated as distinct, leading to
|
||||
// duplicate file loads and memory bloat (issue #2780).
|
||||
func TestGetPackageRealpathFuncs_DuplicateCacheKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Simulate two packages (app-a, app-b) that each have a node_modules symlink to
|
||||
// the same shared dependency. This is a typical pnpm/Bazel layout:
|
||||
// /workspace/packages/app-a/ -> symlink to /store/app-a/
|
||||
// /workspace/packages/app-b/ -> symlink to /store/app-b/
|
||||
// /store/app-a/node_modules/shared-lib -> symlink to /store/shared-lib/
|
||||
// /store/app-b/node_modules/shared-lib -> symlink to /store/shared-lib/
|
||||
fs := vfstest.FromMap(map[string]any{
|
||||
"/workspace/packages/app-a": vfstest.Symlink("/store/app-a"),
|
||||
"/workspace/packages/app-b": vfstest.Symlink("/store/app-b"),
|
||||
"/store/app-a/index.d.ts": "export declare const a: number;",
|
||||
"/store/app-b/index.d.ts": "export declare const b: number;",
|
||||
"/store/app-a/node_modules/shared-lib": vfstest.Symlink("/store/shared-lib"),
|
||||
"/store/app-b/node_modules/shared-lib": vfstest.Symlink("/store/shared-lib"),
|
||||
"/store/shared-lib/index.d.ts": "export declare const shared: string;",
|
||||
}, true)
|
||||
|
||||
toRealpathA, _ := getPackageRealpathFuncs(fs, "/workspace/packages/app-a")
|
||||
toRealpathB, _ := getPackageRealpathFuncs(fs, "/workspace/packages/app-b")
|
||||
|
||||
sharedFileViaA := "/store/app-a/node_modules/shared-lib/index.d.ts"
|
||||
sharedFileViaB := "/store/app-b/node_modules/shared-lib/index.d.ts"
|
||||
|
||||
resolvedA := toRealpathA(sharedFileViaA)
|
||||
resolvedB := toRealpathB(sharedFileViaB)
|
||||
|
||||
// Both should resolve to the same canonical realpath so the module resolver
|
||||
// uses a single cache key for the shared dependency, avoiding duplicate loads.
|
||||
expectedRealpath := "/store/shared-lib/index.d.ts"
|
||||
assert.Equal(t, resolvedA, expectedRealpath,
|
||||
"app-a's toRealpath should follow the node_modules symlink to the realpath")
|
||||
assert.Equal(t, resolvedB, expectedRealpath,
|
||||
"app-b's toRealpath should follow the node_modules symlink to the realpath")
|
||||
}
|
||||
|
||||
// TestGetPackageRealpathFuncs_NonSymlinkedPackageWithSymlinkedDeps tests that even when the
|
||||
// package directory itself is NOT a symlink, toRealpath still follows symlinks for files
|
||||
// outside the package (e.g. re-exports reaching into symlinked node_modules dependencies).
|
||||
func TestGetPackageRealpathFuncs_NonSymlinkedPackageWithSymlinkedDeps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := vfstest.FromMap(map[string]any{
|
||||
"/real/my-pkg/index.d.ts": "export declare const a: number;",
|
||||
"/real/my-pkg/node_modules/dep": vfstest.Symlink("/real/dep"),
|
||||
"/real/dep/index.d.ts": "export declare const b: number;",
|
||||
}, true)
|
||||
|
||||
toRealpath, _ := getPackageRealpathFuncs(fs, "/real/my-pkg")
|
||||
|
||||
// Files inside the (non-symlinked) package should be returned unchanged.
|
||||
assert.Equal(
|
||||
t,
|
||||
toRealpath("/real/my-pkg/index.d.ts"),
|
||||
"/real/my-pkg/index.d.ts",
|
||||
)
|
||||
|
||||
// Files outside the package reached via symlinked node_modules should still be resolved.
|
||||
assert.Equal(
|
||||
t,
|
||||
toRealpath("/real/my-pkg/node_modules/dep/index.d.ts"),
|
||||
"/real/dep/index.d.ts",
|
||||
"symlinked deps must be resolved even when the package dir itself is not a symlink",
|
||||
)
|
||||
}
|
||||
256
tools/tsgo/internal/ls/autoimport/view.go
Normal file
256
tools/tsgo/internal/ls/autoimport/view.go
Normal file
@@ -0,0 +1,256 @@
|
||||
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
|
||||
}
|
||||
99
tools/tsgo/internal/ls/autoinsert.go
Normal file
99
tools/tsgo/internal/ls/autoinsert.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
func (l *LanguageService) ProvideOnAutoInsert(ctx context.Context, params *lsproto.VSOnAutoInsertParams) (lsproto.VSOnAutoInsertResponse, error) {
|
||||
if l.UserPreferences().EnableAutoClosingTags.IsFalse() {
|
||||
return lsproto.VSOnAutoInsertResponse{}, nil
|
||||
}
|
||||
if params.VSCh != ">" {
|
||||
return lsproto.VSOnAutoInsertResponse{}, nil
|
||||
}
|
||||
|
||||
_, sourceFile := l.getProgramAndFile(params.VSTextDocument.Uri)
|
||||
position := l.converters.LineAndCharacterToPosition(sourceFile, params.VSPosition)
|
||||
|
||||
token := astnav.FindPrecedingToken(sourceFile, int(position))
|
||||
if token == nil {
|
||||
return lsproto.VSOnAutoInsertResponse{}, nil
|
||||
}
|
||||
|
||||
var closingText string
|
||||
var element *ast.Node
|
||||
if token.Kind == ast.KindGreaterThanToken && ast.IsJsxOpeningElement(token.Parent) {
|
||||
element = token.Parent.Parent
|
||||
} else if ast.IsJsxText(token) && ast.IsJsxElement(token.Parent) {
|
||||
element = token.Parent
|
||||
}
|
||||
|
||||
if element != nil && isUnclosedTag(element.AsJsxElement()) {
|
||||
tagNameNode := element.AsJsxElement().OpeningElement.TagName()
|
||||
// Slight divergence from Strada - we don't use the verbatim text from the opening tag.
|
||||
closingText = "</" + ast.EntityNameToString(tagNameNode, scanner.GetTextOfNode) + ">"
|
||||
} else {
|
||||
var fragment *ast.Node
|
||||
if token.Kind == ast.KindGreaterThanToken && ast.IsJsxOpeningFragment(token.Parent) {
|
||||
fragment = token.Parent.Parent
|
||||
} else if ast.IsJsxText(token) && ast.IsJsxFragment(token.Parent) {
|
||||
fragment = token.Parent
|
||||
}
|
||||
|
||||
if fragment != nil && isUnclosedFragment(fragment.AsJsxFragment()) {
|
||||
closingText = "</>"
|
||||
}
|
||||
}
|
||||
|
||||
if closingText == "" {
|
||||
return lsproto.VSOnAutoInsertResponse{}, nil
|
||||
}
|
||||
|
||||
return lsproto.VSOnAutoInsertResponse{
|
||||
VSOnAutoInsertResponseItem: &lsproto.VSOnAutoInsertResponseItem{
|
||||
VSTextEditFormat: lsproto.InsertTextFormatSnippet,
|
||||
VSTextEdit: &lsproto.TextEdit{
|
||||
Range: lsproto.Range{Start: params.VSPosition, End: params.VSPosition},
|
||||
// Tag names can contain `$` (valid JSX identifier characters), so
|
||||
// escape the closing text to avoid being interpreted as a snippet
|
||||
// placeholder/variable.
|
||||
NewText: "$0" + escapeSnippetText(closingText),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isUnclosedTag(node *ast.JsxElement) bool {
|
||||
openingElement := node.OpeningElement
|
||||
closingElement := node.ClosingElement
|
||||
if !ast.TagNamesAreEquivalent(openingElement.TagName(), closingElement.TagName()) {
|
||||
return true
|
||||
}
|
||||
|
||||
parent := node.Parent
|
||||
if ast.IsJsxElement(parent) {
|
||||
parent := parent.AsJsxElement()
|
||||
return ast.TagNamesAreEquivalent(openingElement.TagName(), parent.OpeningElement.TagName()) && isUnclosedTag(parent)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isUnclosedFragment(node *ast.JsxFragment) bool {
|
||||
closingFragment := node.ClosingFragment
|
||||
if closingFragment.Flags&ast.NodeFlagsThisNodeHasError != 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
parent := node.Parent
|
||||
if ast.IsJsxFragment(parent) && isUnclosedFragment(parent.AsJsxFragment()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
1103
tools/tsgo/internal/ls/callhierarchy.go
Normal file
1103
tools/tsgo/internal/ls/callhierarchy.go
Normal file
File diff suppressed because it is too large
Load Diff
270
tools/tsgo/internal/ls/change/delete.go
Normal file
270
tools/tsgo/internal/ls/change/delete.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/debug"
|
||||
"github.com/microsoft/typescript-go/internal/format"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
// deleteDeclaration deletes a node with smart handling for different node types.
|
||||
// This handles special cases like import specifiers in lists, parameters, etc.
|
||||
func deleteDeclaration(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) {
|
||||
switch node.Kind {
|
||||
case ast.KindParameter:
|
||||
oldFunction := node.Parent
|
||||
if oldFunction.Kind == ast.KindArrowFunction &&
|
||||
len(oldFunction.AsArrowFunction().Parameters.Nodes) == 1 &&
|
||||
astnav.FindChildOfKind(oldFunction, ast.KindOpenParenToken, sourceFile) == nil {
|
||||
// Lambdas with exactly one parameter are special because, after removal, there
|
||||
// must be an empty parameter list (i.e. `()`) and this won't necessarily be the
|
||||
// case if the parameter is simply removed (e.g. in `x => 1`).
|
||||
t.ReplaceRangeWithText(sourceFile, t.GetAdjustedRange(sourceFile, node, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude), "()")
|
||||
} else {
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
}
|
||||
|
||||
case ast.KindImportDeclaration, ast.KindImportEqualsDeclaration:
|
||||
imports := sourceFile.Imports()
|
||||
isFirstImport := len(imports) > 0 && node == imports[0].Parent ||
|
||||
node == core.Find(sourceFile.Statements.Nodes, func(s *ast.Node) bool { return ast.IsAnyImportSyntax(s) })
|
||||
// For first import, leave header comment in place, otherwise only delete JSDoc comments
|
||||
leadingTrivia := LeadingTriviaOptionStartLine
|
||||
if isFirstImport {
|
||||
leadingTrivia = LeadingTriviaOptionExclude
|
||||
} else if hasJSDocNodes(node) {
|
||||
leadingTrivia = LeadingTriviaOptionJSDoc
|
||||
}
|
||||
deleteNode(t, sourceFile, node, leadingTrivia, TrailingTriviaOptionInclude)
|
||||
|
||||
case ast.KindBindingElement:
|
||||
pattern := node.Parent
|
||||
preserveComma := pattern.Kind == ast.KindArrayBindingPattern &&
|
||||
node != pattern.AsBindingPattern().Elements.Nodes[len(pattern.AsBindingPattern().Elements.Nodes)-1]
|
||||
if preserveComma {
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionExclude)
|
||||
} else {
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
}
|
||||
|
||||
case ast.KindVariableDeclaration:
|
||||
deleteVariableDeclaration(t, deletedNodesInLists, sourceFile, node)
|
||||
|
||||
case ast.KindTypeParameter:
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
|
||||
case ast.KindImportSpecifier:
|
||||
namedImports := node.Parent
|
||||
if len(namedImports.AsNamedImports().Elements.Nodes) == 1 {
|
||||
deleteImportBinding(t, sourceFile, namedImports)
|
||||
} else {
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
}
|
||||
|
||||
case ast.KindNamespaceImport:
|
||||
deleteImportBinding(t, sourceFile, node)
|
||||
|
||||
case ast.KindSemicolonToken:
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionExclude)
|
||||
|
||||
case ast.KindTypeKeyword:
|
||||
// For type keyword in import clauses, we need to delete the keyword and any trailing space
|
||||
// The trailing space is part of the next token's leading trivia, so we include it
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionExclude, TrailingTriviaOptionInclude)
|
||||
|
||||
case ast.KindFunctionKeyword:
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionExclude, TrailingTriviaOptionInclude)
|
||||
|
||||
case ast.KindClassDeclaration, ast.KindFunctionDeclaration:
|
||||
leadingTrivia := LeadingTriviaOptionStartLine
|
||||
if hasJSDocNodes(node) {
|
||||
leadingTrivia = LeadingTriviaOptionJSDoc
|
||||
}
|
||||
deleteNode(t, sourceFile, node, leadingTrivia, TrailingTriviaOptionInclude)
|
||||
|
||||
default:
|
||||
if node.Parent == nil {
|
||||
// a misbehaving client can reach here with the SourceFile node
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
} else if node.Parent.Kind == ast.KindImportClause && node.Parent.AsImportClause().Name() == node {
|
||||
deleteDefaultImport(t, sourceFile, node.Parent)
|
||||
} else if node.Parent.Kind == ast.KindCallExpression && slices.Contains(node.Parent.AsCallExpression().Arguments.Nodes, node) {
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
} else {
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteDefaultImport(t *Tracker, sourceFile *ast.SourceFile, importClause *ast.Node) {
|
||||
clause := importClause.AsImportClause()
|
||||
if clause.NamedBindings == nil {
|
||||
// Delete the whole import
|
||||
deleteNode(t, sourceFile, importClause.Parent, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
} else {
|
||||
// import |d,| * as ns from './file'
|
||||
name := clause.Name()
|
||||
start := astnav.GetStartOfNode(name, sourceFile, false)
|
||||
nextToken := astnav.GetTokenAtPosition(sourceFile, name.End())
|
||||
if nextToken != nil && nextToken.Kind == ast.KindCommaToken {
|
||||
// shift first non-whitespace position after comma to the start position of the node
|
||||
end := scanner.SkipTriviaEx(sourceFile.Text(), nextToken.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: false, StopAtComments: true})
|
||||
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(start))
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(end))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
|
||||
} else {
|
||||
deleteNode(t, sourceFile, name, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteImportBinding(t *Tracker, sourceFile *ast.SourceFile, node *ast.Node) {
|
||||
importClause := node.Parent.AsImportClause()
|
||||
if importClause.Name() != nil {
|
||||
// Delete named imports while preserving the default import
|
||||
// import d|, * as ns| from './file'
|
||||
// import d|, { a }| from './file'
|
||||
previousToken := astnav.GetTokenAtPosition(sourceFile, node.Pos()-1)
|
||||
debug.Assert(previousToken != nil, "previousToken should not be nil")
|
||||
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(astnav.GetStartOfNode(previousToken, sourceFile, false)))
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(node.End()))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
|
||||
} else {
|
||||
// Delete the entire import declaration
|
||||
// |import * as ns from './file'|
|
||||
// |import { a } from './file'|
|
||||
importDecl := ast.FindAncestorKind(node, ast.KindImportDeclaration)
|
||||
debug.Assert(importDecl != nil, "importDecl should not be nil")
|
||||
deleteNode(t, sourceFile, importDecl, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteVariableDeclaration(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) {
|
||||
parent := node.Parent
|
||||
|
||||
if parent.Kind == ast.KindCatchClause {
|
||||
// TODO: There's currently no unused diagnostic for this, could be a suggestion
|
||||
openParen := astnav.FindChildOfKind(parent, ast.KindOpenParenToken, sourceFile)
|
||||
closeParen := astnav.FindChildOfKind(parent, ast.KindCloseParenToken, sourceFile)
|
||||
debug.Assert(openParen != nil && closeParen != nil, "catch clause should have parens")
|
||||
t.DeleteNodeRange(sourceFile, openParen, closeParen, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
return
|
||||
}
|
||||
|
||||
if len(parent.AsVariableDeclarationList().Declarations.Nodes) != 1 {
|
||||
deleteNodeInList(t, deletedNodesInLists, sourceFile, node)
|
||||
return
|
||||
}
|
||||
|
||||
gp := parent.Parent
|
||||
switch gp.Kind {
|
||||
case ast.KindForOfStatement, ast.KindForInStatement:
|
||||
t.ReplaceNode(sourceFile, node, t.NodeFactory.NewObjectLiteralExpression(t.NodeFactory.NewNodeList([]*ast.Node{}), false), nil)
|
||||
|
||||
case ast.KindForStatement:
|
||||
deleteNode(t, sourceFile, parent, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
|
||||
case ast.KindVariableStatement:
|
||||
leadingTrivia := LeadingTriviaOptionStartLine
|
||||
if hasJSDocNodes(gp) {
|
||||
leadingTrivia = LeadingTriviaOptionJSDoc
|
||||
}
|
||||
deleteNode(t, sourceFile, gp, leadingTrivia, TrailingTriviaOptionInclude)
|
||||
|
||||
default:
|
||||
debug.Fail("Unexpected grandparent kind: " + gp.Kind.String())
|
||||
}
|
||||
}
|
||||
|
||||
// deleteNode deletes a node with the specified trivia options.
|
||||
// Warning: This deletes comments too.
|
||||
func deleteNode(t *Tracker, sourceFile *ast.SourceFile, node *ast.Node, leadingTrivia LeadingTriviaOption, trailingTrivia TrailingTriviaOption) {
|
||||
startPosition := t.getAdjustedStartPosition(sourceFile, node, leadingTrivia, false)
|
||||
endPosition := t.getAdjustedEndPosition(sourceFile, node, trailingTrivia)
|
||||
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPosition))
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(endPosition))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
|
||||
}
|
||||
|
||||
func deleteNodeInList(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) {
|
||||
containingList := format.GetContainingList(node, sourceFile)
|
||||
debug.Assert(containingList != nil, "containingList should not be nil")
|
||||
index := slices.Index(containingList.Nodes, node)
|
||||
debug.Assert(index != -1, "node should be in containing list")
|
||||
|
||||
if len(containingList.Nodes) == 1 {
|
||||
deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude)
|
||||
return
|
||||
}
|
||||
|
||||
// Note: We will only delete a comma *after* a node. This will leave a trailing comma if we delete the last node.
|
||||
// That's handled in the end by finishTrailingCommaAfterDeletingNodesInList.
|
||||
debug.Assert(!deletedNodesInLists[node], "Deleting a node twice")
|
||||
deletedNodesInLists[node] = true
|
||||
|
||||
startPos := t.startPositionToDeleteNodeInList(sourceFile, node)
|
||||
var endPos int
|
||||
if index == len(containingList.Nodes)-1 {
|
||||
endPos = t.getAdjustedEndPosition(sourceFile, node, TrailingTriviaOptionNone)
|
||||
} else {
|
||||
prevNode := (*ast.Node)(nil)
|
||||
if index > 0 {
|
||||
prevNode = containingList.Nodes[index-1]
|
||||
}
|
||||
endPos = t.endPositionToDeleteNodeInList(sourceFile, node, prevNode, containingList.Nodes[index+1])
|
||||
}
|
||||
|
||||
startLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPos))
|
||||
endLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(endPos))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startLSPos, End: endLSPos}, "")
|
||||
}
|
||||
|
||||
// startPositionToDeleteNodeInList finds the first non-whitespace position in the leading trivia of the node
|
||||
func (t *Tracker) startPositionToDeleteNodeInList(sourceFile *ast.SourceFile, node *ast.Node) int {
|
||||
start := t.getAdjustedStartPosition(sourceFile, node, LeadingTriviaOptionIncludeAll, false)
|
||||
return scanner.SkipTriviaEx(sourceFile.Text(), start, &scanner.SkipTriviaOptions{StopAfterLineBreak: false, StopAtComments: true})
|
||||
}
|
||||
|
||||
func (t *Tracker) endPositionToDeleteNodeInList(sourceFile *ast.SourceFile, node *ast.Node, prevNode *ast.Node, nextNode *ast.Node) int {
|
||||
end := t.startPositionToDeleteNodeInList(sourceFile, nextNode)
|
||||
if prevNode == nil || positionsAreOnSameLine(t.getAdjustedEndPosition(sourceFile, node, TrailingTriviaOptionInclude), end, sourceFile) {
|
||||
return end
|
||||
}
|
||||
token := astnav.FindPrecedingToken(sourceFile, astnav.GetStartOfNode(nextNode, sourceFile, false))
|
||||
if isSeparator(node, token) {
|
||||
prevToken := astnav.FindPrecedingToken(sourceFile, astnav.GetStartOfNode(node, sourceFile, false))
|
||||
if isSeparator(prevNode, prevToken) {
|
||||
pos := scanner.SkipTriviaEx(sourceFile.Text(), token.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true})
|
||||
if positionsAreOnSameLine(astnav.GetStartOfNode(prevToken, sourceFile, false), astnav.GetStartOfNode(token, sourceFile, false), sourceFile) {
|
||||
if pos > 0 && stringutil.IsLineBreak(rune(sourceFile.Text()[pos-1])) {
|
||||
return pos - 1
|
||||
}
|
||||
return pos
|
||||
}
|
||||
if stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) {
|
||||
return pos
|
||||
}
|
||||
}
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func positionsAreOnSameLine(pos1, pos2 int, sourceFile *ast.SourceFile) bool {
|
||||
return format.GetLineStartPositionForPosition(pos1, sourceFile) == format.GetLineStartPositionForPosition(pos2, sourceFile)
|
||||
}
|
||||
|
||||
// hasJSDocNodes checks if a node has JSDoc comments
|
||||
func hasJSDocNodes(node *ast.Node) bool {
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
// nil is ok for JSDoc - it will return empty slice if not available
|
||||
jsdocs := node.JSDoc(nil)
|
||||
return len(jsdocs) > 0
|
||||
}
|
||||
751
tools/tsgo/internal/ls/change/tracker.go
Normal file
751
tools/tsgo/internal/ls/change/tracker.go
Normal file
@@ -0,0 +1,751 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/format"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
type NodeOptions struct {
|
||||
// Text to be inserted before the new node
|
||||
Prefix string
|
||||
|
||||
// Text to be inserted after the new node
|
||||
Suffix string
|
||||
|
||||
// Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node
|
||||
indentation *int
|
||||
|
||||
// Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind
|
||||
delta *int
|
||||
|
||||
LeadingTriviaOption
|
||||
TrailingTriviaOption
|
||||
joiner string
|
||||
}
|
||||
|
||||
type LeadingTriviaOption int
|
||||
|
||||
const (
|
||||
LeadingTriviaOptionNone LeadingTriviaOption = 0
|
||||
LeadingTriviaOptionExclude LeadingTriviaOption = 1
|
||||
LeadingTriviaOptionIncludeAll LeadingTriviaOption = 2
|
||||
LeadingTriviaOptionJSDoc LeadingTriviaOption = 3
|
||||
LeadingTriviaOptionStartLine LeadingTriviaOption = 4
|
||||
)
|
||||
|
||||
type TrailingTriviaOption int
|
||||
|
||||
const (
|
||||
TrailingTriviaOptionNone TrailingTriviaOption = 0
|
||||
TrailingTriviaOptionExclude TrailingTriviaOption = 1
|
||||
TrailingTriviaOptionExcludeWhitespace TrailingTriviaOption = 2
|
||||
TrailingTriviaOptionInclude TrailingTriviaOption = 3
|
||||
)
|
||||
|
||||
type trackerEditKind int
|
||||
|
||||
const (
|
||||
trackerEditKindText trackerEditKind = 1
|
||||
trackerEditKindRemove trackerEditKind = 2
|
||||
trackerEditKindReplaceWithSingleNode trackerEditKind = 3
|
||||
trackerEditKindReplaceWithMultipleNodes trackerEditKind = 4
|
||||
)
|
||||
|
||||
type trackerEdit struct {
|
||||
kind trackerEditKind
|
||||
lsproto.Range
|
||||
|
||||
NewText string // kind == text
|
||||
|
||||
*ast.Node // single
|
||||
nodes []*ast.Node // multiple
|
||||
options NodeOptions
|
||||
}
|
||||
|
||||
type nodesInsertedAtStartState struct {
|
||||
node *ast.Node
|
||||
sourceFile *ast.SourceFile
|
||||
}
|
||||
|
||||
type Tracker struct {
|
||||
// initialized with
|
||||
formatSettings lsutil.FormatCodeSettings
|
||||
newLine string
|
||||
converters *lsconv.Converters
|
||||
ctx context.Context
|
||||
*printer.EmitContext
|
||||
|
||||
*ast.NodeFactory
|
||||
|
||||
changes *collections.MultiMap[*ast.SourceFile, *trackerEdit]
|
||||
deletedNodes []deletedNode
|
||||
nodesWithInsertionsAtStart map[*ast.Node]*nodesInsertedAtStartState
|
||||
|
||||
// created during call to getChanges
|
||||
writer *printer.ChangeTrackerWriter
|
||||
// printer
|
||||
}
|
||||
|
||||
type deletedNode struct {
|
||||
sourceFile *ast.SourceFile
|
||||
node *ast.Node
|
||||
}
|
||||
|
||||
func NewTracker(ctx context.Context, compilerOptions *core.CompilerOptions, formatOptions lsutil.FormatCodeSettings, converters *lsconv.Converters) *Tracker {
|
||||
emitContext := printer.NewEmitContext()
|
||||
newLine := compilerOptions.NewLine.GetNewLineCharacter()
|
||||
ctx = format.WithFormatCodeSettings(ctx, formatOptions, newLine) // !!! formatSettings in context?
|
||||
return &Tracker{
|
||||
EmitContext: emitContext,
|
||||
NodeFactory: &emitContext.Factory.NodeFactory,
|
||||
changes: &collections.MultiMap[*ast.SourceFile, *trackerEdit]{},
|
||||
ctx: ctx,
|
||||
converters: converters,
|
||||
formatSettings: formatOptions,
|
||||
newLine: newLine,
|
||||
nodesWithInsertionsAtStart: make(map[*ast.Node]*nodesInsertedAtStartState),
|
||||
}
|
||||
}
|
||||
|
||||
// GetChanges returns the accumulated text edits.
|
||||
// Note: after calling this, the Tracker object must be discarded!
|
||||
func (t *Tracker) GetChanges() map[string][]*lsproto.TextEdit {
|
||||
t.finishDeleteDeclarations()
|
||||
t.finishNodesWithInsertionsAtStart()
|
||||
changes := t.getTextChangesFromChanges()
|
||||
// !!! changes for new files
|
||||
return changes
|
||||
}
|
||||
|
||||
func (t *Tracker) ReplaceNode(sourceFile *ast.SourceFile, oldNode *ast.Node, newNode *ast.Node, options *NodeOptions) {
|
||||
if options == nil {
|
||||
// defaults to `useNonAdjustedPositions`
|
||||
options = &NodeOptions{
|
||||
LeadingTriviaOption: LeadingTriviaOptionExclude,
|
||||
TrailingTriviaOption: TrailingTriviaOptionExclude,
|
||||
}
|
||||
}
|
||||
t.ReplaceRange(sourceFile, t.GetAdjustedRange(sourceFile, oldNode, oldNode, options.LeadingTriviaOption, options.TrailingTriviaOption), newNode, *options)
|
||||
}
|
||||
|
||||
func (t *Tracker) ReplaceNodeWithNodes(sourceFile *ast.SourceFile, oldNode *ast.Node, newNodes []*ast.Node, options *NodeOptions) {
|
||||
if options == nil {
|
||||
options = &NodeOptions{
|
||||
LeadingTriviaOption: LeadingTriviaOptionExclude,
|
||||
TrailingTriviaOption: TrailingTriviaOptionExclude,
|
||||
}
|
||||
}
|
||||
t.ReplaceRangeWithNodes(sourceFile, t.GetAdjustedRange(sourceFile, oldNode, oldNode, options.LeadingTriviaOption, options.TrailingTriviaOption), newNodes, *options)
|
||||
}
|
||||
|
||||
func (t *Tracker) ReplaceRange(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNode *ast.Node, options NodeOptions) {
|
||||
t.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithSingleNode, Range: lsprotoRange, options: options, Node: newNode})
|
||||
}
|
||||
|
||||
func (t *Tracker) ReplaceRangeWithText(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, text string) {
|
||||
t.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindText, Range: lsprotoRange, NewText: text})
|
||||
}
|
||||
|
||||
func (t *Tracker) ReplaceRangeWithNodes(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNodes []*ast.Node, options NodeOptions) {
|
||||
if len(newNodes) == 1 {
|
||||
t.ReplaceRange(sourceFile, lsprotoRange, newNodes[0], options)
|
||||
return
|
||||
}
|
||||
t.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithMultipleNodes, Range: lsprotoRange, nodes: newNodes, options: options})
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertText(sourceFile *ast.SourceFile, pos lsproto.Position, text string) {
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: pos, End: pos}, text)
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertNodeAt(sourceFile *ast.SourceFile, pos core.TextPos, newNode *ast.Node, options NodeOptions) {
|
||||
lsPos := t.converters.PositionToLineAndCharacter(sourceFile, pos)
|
||||
t.ReplaceRange(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNode, options)
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertNodesAt(sourceFile *ast.SourceFile, pos core.TextPos, newNodes []*ast.Node, options NodeOptions) {
|
||||
lsPos := t.converters.PositionToLineAndCharacter(sourceFile, pos)
|
||||
t.ReplaceRangeWithNodes(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNodes, options)
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) {
|
||||
endPosition := t.endPosForInsertNodeAfter(sourceFile, after, newNode)
|
||||
t.InsertNodeAt(sourceFile, endPosition, newNode, t.getInsertNodeAfterOptions(sourceFile, after))
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertNodesAfter(sourceFile *ast.SourceFile, after *ast.Node, newNodes []*ast.Node) {
|
||||
endPosition := t.endPosForInsertNodeAfter(sourceFile, after, newNodes[0])
|
||||
t.InsertNodesAt(sourceFile, endPosition, newNodes, t.getInsertNodeAfterOptions(sourceFile, after))
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertNodeBefore(sourceFile *ast.SourceFile, before *ast.Node, newNode *ast.Node, blankLineBetween bool, leadingTriviaOption LeadingTriviaOption) {
|
||||
t.InsertNodeAt(sourceFile, core.TextPos(t.getAdjustedStartPosition(sourceFile, before, leadingTriviaOption, false)), newNode, t.getOptionsForInsertNodeBefore(before, newNode, blankLineBetween))
|
||||
}
|
||||
|
||||
// TryInsertTypeAnnotation inserts a type annotation after the appropriate position on a node
|
||||
// (after the close paren for function-like, after the name/exclamation/question for variable-like).
|
||||
// Returns true if successful.
|
||||
func (t *Tracker) TryInsertTypeAnnotation(sourceFile *ast.SourceFile, node *ast.Node, typeNode *ast.Node) bool {
|
||||
var endNode *ast.Node
|
||||
if ast.IsFunctionLike(node) {
|
||||
endNode = astnav.FindChildOfKind(node, ast.KindCloseParenToken, sourceFile)
|
||||
if endNode == nil {
|
||||
if !ast.IsArrowFunction(node) {
|
||||
return false
|
||||
}
|
||||
// If no `)`, is an arrow function `x => x`, so use the end of the first parameter
|
||||
params := node.Parameters()
|
||||
if len(params) == 0 {
|
||||
return false
|
||||
}
|
||||
endNode = params[0]
|
||||
}
|
||||
} else {
|
||||
switch node.Kind {
|
||||
case ast.KindVariableDeclaration:
|
||||
endNode = node.AsVariableDeclaration().ExclamationToken
|
||||
case ast.KindPropertySignature:
|
||||
endNode = node.AsPropertySignatureDeclaration().PostfixToken
|
||||
case ast.KindPropertyDeclaration:
|
||||
endNode = node.AsPropertyDeclaration().PostfixToken
|
||||
case ast.KindParameter:
|
||||
endNode = node.AsParameterDeclaration().QuestionToken
|
||||
}
|
||||
if endNode == nil {
|
||||
endNode = node.Name()
|
||||
}
|
||||
}
|
||||
if endNode == nil {
|
||||
return false
|
||||
}
|
||||
t.InsertNodeAt(sourceFile, core.TextPos(endNode.End()), typeNode, NodeOptions{Prefix: ": "})
|
||||
return true
|
||||
}
|
||||
|
||||
// ParenthesizeArrowParameters wraps the parameters of a paren-less arrow function in `(` and `)`.
|
||||
// This is a no-op if the arrow function already has parens.
|
||||
func (t *Tracker) ParenthesizeArrowParameters(sourceFile *ast.SourceFile, arrowFunc *ast.Node) {
|
||||
if astnav.FindChildOfKind(arrowFunc, ast.KindCloseParenToken, sourceFile) != nil {
|
||||
return
|
||||
}
|
||||
params := arrowFunc.Parameters()
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
firstParam := params[0]
|
||||
lastParam := params[len(params)-1]
|
||||
startPos := astnav.GetStartOfNode(firstParam, sourceFile, false)
|
||||
t.InsertText(sourceFile, t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPos)), "(")
|
||||
t.InsertText(sourceFile, t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(lastParam.End())), ")")
|
||||
}
|
||||
|
||||
// InsertModifierBefore inserts a modifier token (like 'type') before a node with a trailing space.
|
||||
func (t *Tracker) InsertModifierBefore(sourceFile *ast.SourceFile, modifier ast.Kind, before *ast.Node) {
|
||||
pos := astnav.GetStartOfNode(before, sourceFile, false)
|
||||
token := t.NewToken(modifier)
|
||||
token.Loc = core.NewTextRange(pos, pos)
|
||||
token.Parent = before.Parent
|
||||
t.InsertNodeAt(sourceFile, core.TextPos(pos), token, NodeOptions{Suffix: " "})
|
||||
}
|
||||
|
||||
// Delete queues a node for deletion with smart handling of list items, imports, etc.
|
||||
// The actual deletion happens in finishDeleteDeclarations during GetChanges.
|
||||
func (t *Tracker) Delete(sourceFile *ast.SourceFile, node *ast.Node) {
|
||||
t.deletedNodes = append(t.deletedNodes, deletedNode{sourceFile: sourceFile, node: node})
|
||||
}
|
||||
|
||||
// DeleteRange deletes a text range from the source file.
|
||||
func (t *Tracker) DeleteRange(sourceFile *ast.SourceFile, textRange core.TextRange) {
|
||||
lspRange := t.converters.ToLSPRange(sourceFile, textRange)
|
||||
t.ReplaceRangeWithText(sourceFile, lspRange, "")
|
||||
}
|
||||
|
||||
// DeleteNode deletes a node immediately with specified trivia options.
|
||||
// Stop! Consider using Delete instead, which has logic for deleting nodes from delimited lists.
|
||||
func (t *Tracker) DeleteNode(sourceFile *ast.SourceFile, node *ast.Node, leadingTrivia LeadingTriviaOption, trailingTrivia TrailingTriviaOption) {
|
||||
rng := t.GetAdjustedRange(sourceFile, node, node, leadingTrivia, trailingTrivia)
|
||||
t.ReplaceRangeWithText(sourceFile, rng, "")
|
||||
}
|
||||
|
||||
// DeleteNodeRange deletes a range of nodes with specified trivia options.
|
||||
func (t *Tracker) DeleteNodeRange(sourceFile *ast.SourceFile, startNode *ast.Node, endNode *ast.Node, leadingTrivia LeadingTriviaOption, trailingTrivia TrailingTriviaOption) {
|
||||
startPosition := t.getAdjustedStartPosition(sourceFile, startNode, leadingTrivia, false)
|
||||
endPosition := t.getAdjustedEndPosition(sourceFile, endNode, trailingTrivia)
|
||||
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPosition))
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(endPosition))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
|
||||
}
|
||||
|
||||
// finishDeleteDeclarations processes all queued deletions with smart handling for lists and trailing commas.
|
||||
func (t *Tracker) finishDeleteDeclarations() {
|
||||
deletedNodesInLists := make(map[*ast.Node]bool)
|
||||
|
||||
for _, deleted := range t.deletedNodes {
|
||||
// Skip if this node is contained within another deleted node
|
||||
isContained := false
|
||||
for _, other := range t.deletedNodes {
|
||||
if other.sourceFile == deleted.sourceFile && other.node != deleted.node &&
|
||||
rangeContainsRangeExclusive(other.node, deleted.node) {
|
||||
isContained = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isContained {
|
||||
continue
|
||||
}
|
||||
|
||||
deleteDeclaration(t, deletedNodesInLists, deleted.sourceFile, deleted.node)
|
||||
}
|
||||
|
||||
// Handle trailing commas for last elements in lists
|
||||
for node := range deletedNodesInLists {
|
||||
sourceFile := ast.GetSourceFileOfNode(node)
|
||||
list := format.GetContainingList(node, sourceFile)
|
||||
if list == nil || node != list.Nodes[len(list.Nodes)-1] {
|
||||
continue
|
||||
}
|
||||
|
||||
lastNonDeletedIndex := -1
|
||||
for i := len(list.Nodes) - 2; i >= 0; i-- {
|
||||
if !deletedNodesInLists[list.Nodes[i]] {
|
||||
lastNonDeletedIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lastNonDeletedIndex != -1 {
|
||||
startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(list.Nodes[lastNonDeletedIndex].End()))
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(t.startPositionToDeleteNodeInList(sourceFile, list.Nodes[lastNonDeletedIndex+1])))
|
||||
t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) endPosForInsertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) core.TextPos {
|
||||
if needSemicolonBetween(after, newNode) && (rune(sourceFile.Text()[after.End()-1]) != ';') {
|
||||
// check if previous statement ends with semicolon
|
||||
// if not - insert semicolon to preserve the code from changing the meaning due to ASI
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End()))
|
||||
semicolon := t.NewToken(ast.KindSemicolonToken)
|
||||
semicolon.Loc = core.NewTextRange(after.End(), after.End())
|
||||
semicolon.Parent = after.Parent
|
||||
t.ReplaceRange(
|
||||
sourceFile,
|
||||
lsproto.Range{Start: endPos, End: endPos},
|
||||
semicolon,
|
||||
NodeOptions{},
|
||||
)
|
||||
}
|
||||
return core.TextPos(t.getAdjustedEndPosition(sourceFile, after, TrailingTriviaOptionNone))
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range,
|
||||
* i.e. arguments in arguments lists, parameters in parameter lists etc.
|
||||
* Note that separators are part of the node in statements and class elements.
|
||||
*/
|
||||
func (t *Tracker) InsertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node, containingList *ast.NodeList) {
|
||||
if containingList == nil {
|
||||
containingList = format.GetContainingList(after, sourceFile)
|
||||
}
|
||||
if containingList == nil {
|
||||
// Debug.fail("node is not a list element")
|
||||
return
|
||||
}
|
||||
index := slices.Index(containingList.Nodes, after)
|
||||
if index < 0 {
|
||||
return
|
||||
}
|
||||
end := after.End()
|
||||
if index != len(containingList.Nodes)-1 {
|
||||
// any element except the last one
|
||||
// use next sibling as an anchor
|
||||
if nextToken := astnav.GetTokenAtPosition(sourceFile, after.End()); nextToken != nil && isSeparator(after, nextToken) {
|
||||
// for list
|
||||
// a, b, c
|
||||
// create change for adding 'e' after 'a' as
|
||||
// - find start of next element after a (it is b)
|
||||
// - use next element start as start and end position in final change
|
||||
// - build text of change by formatting the text of node + whitespace trivia of b
|
||||
|
||||
// in multiline case it will work as
|
||||
// a,
|
||||
// b,
|
||||
// c,
|
||||
// result - '*' denotes leading trivia that will be inserted after new text (displayed as '#')
|
||||
// a,
|
||||
// insertedtext<separator>#
|
||||
// ###b,
|
||||
// c,
|
||||
nextNode := containingList.Nodes[index+1]
|
||||
startPos := scanner.SkipTriviaEx(sourceFile.Text(), nextNode.Pos(), &scanner.SkipTriviaOptions{StopAfterLineBreak: false, StopAtComments: true})
|
||||
|
||||
// write separator and leading trivia of the next element as suffix
|
||||
suffix := scanner.TokenToString(nextToken.Kind) + sourceFile.Text()[nextToken.End():startPos]
|
||||
t.InsertNodesAt(sourceFile, core.TextPos(startPos), []*ast.Node{newNode}, NodeOptions{Suffix: suffix})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
afterStart := astnav.GetStartOfNode(after, sourceFile, false)
|
||||
afterStartLinePosition := format.GetLineStartPositionForPosition(afterStart, sourceFile)
|
||||
|
||||
// insert element after the last element in the list that has more than one item
|
||||
// pick the element preceding the after element to:
|
||||
// - pick the separator
|
||||
// - determine if list is a multiline
|
||||
multilineList := false
|
||||
|
||||
// if list has only one element then we'll format is as multiline if node has comment in trailing trivia, or as singleline otherwise
|
||||
// i.e. var x = 1 // this is x
|
||||
// | new element will be inserted at this position
|
||||
separator := ast.KindCommaToken // SyntaxKind.CommaToken | SyntaxKind.SemicolonToken
|
||||
if len(containingList.Nodes) != 1 {
|
||||
// otherwise, if list has more than one element, pick separator from the list
|
||||
tokenBeforeInsertPosition := astnav.FindPrecedingToken(sourceFile, after.Pos())
|
||||
separator = core.IfElse(isSeparator(after, tokenBeforeInsertPosition), tokenBeforeInsertPosition.Kind, ast.KindCommaToken)
|
||||
// determine if list is multiline by checking lines of after element and element that precedes it.
|
||||
afterMinusOneStartLinePosition := format.GetLineStartPositionForPosition(astnav.GetStartOfNode(containingList.Nodes[index-1], sourceFile, false), sourceFile)
|
||||
multilineList = afterMinusOneStartLinePosition != afterStartLinePosition
|
||||
}
|
||||
if hasCommentsBeforeLineBreak(sourceFile.Text(), after.End()) || !positionsAreOnSameLine(containingList.Pos(), containingList.End(), sourceFile) {
|
||||
// in this case we'll always treat containing list as multiline
|
||||
multilineList = true
|
||||
}
|
||||
if multilineList {
|
||||
// insert separator immediately following the 'after' node to preserve comments in trailing trivia
|
||||
separatorToken := t.NewToken(separator)
|
||||
separatorString := scanner.TokenToString(separator)
|
||||
separatorToken.Loc = core.NewTextRange(end, end+len(separatorString))
|
||||
separatorToken.Parent = after.Parent
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(end))
|
||||
t.ReplaceRange(sourceFile, lsproto.Range{Start: endPos, End: endPos}, separatorToken, NodeOptions{})
|
||||
// use the same indentation as 'after' item
|
||||
indentation := format.FindFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, t.formatSettings)
|
||||
// insert element before the line break on the line that contains 'after' element
|
||||
insertPos := scanner.SkipTriviaEx(sourceFile.Text(), end, &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: false})
|
||||
// find position before "\n" or "\r\n"
|
||||
for insertPos != end && stringutil.IsLineBreak(rune(sourceFile.Text()[insertPos-1])) {
|
||||
insertPos--
|
||||
}
|
||||
insertLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(insertPos))
|
||||
t.ReplaceRange(
|
||||
sourceFile,
|
||||
lsproto.Range{Start: insertLSPos, End: insertLSPos},
|
||||
newNode,
|
||||
NodeOptions{
|
||||
indentation: &indentation,
|
||||
Prefix: t.newLine,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
separatorString := scanner.TokenToString(separator)
|
||||
endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(end))
|
||||
t.ReplaceRange(sourceFile, lsproto.Range{Start: endPos, End: endPos}, newNode, NodeOptions{Prefix: separatorString + " "})
|
||||
}
|
||||
}
|
||||
|
||||
// InsertImportSpecifierAtIndex inserts a new import specifier at the specified index in a NamedImports list
|
||||
func (t *Tracker) InsertImportSpecifierAtIndex(sourceFile *ast.SourceFile, newSpecifier *ast.Node, namedImports *ast.Node, index int) {
|
||||
namedImportsNode := namedImports.AsNamedImports()
|
||||
elements := namedImportsNode.Elements.Nodes
|
||||
|
||||
var prevSpecifier *ast.Node
|
||||
if index > 0 && index-1 < len(elements) {
|
||||
prevSpecifier = elements[index-1]
|
||||
}
|
||||
if prevSpecifier != nil {
|
||||
t.InsertNodeInListAfter(sourceFile, prevSpecifier, newSpecifier, nil)
|
||||
} else {
|
||||
t.InsertNodeBefore(
|
||||
sourceFile,
|
||||
elements[0],
|
||||
newSpecifier,
|
||||
!positionsAreOnSameLine(astnav.GetStartOfNode(elements[0], sourceFile, false), astnav.GetStartOfNode(namedImports.Parent.Parent, sourceFile, false), sourceFile),
|
||||
LeadingTriviaOptionNone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertAtTopOfFile(sourceFile *ast.SourceFile, insert []*ast.Statement, blankLineBetween bool) {
|
||||
if len(insert) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
pos := t.getInsertionPositionAtSourceFileTop(sourceFile)
|
||||
options := NodeOptions{}
|
||||
if pos != 0 {
|
||||
options.Prefix = t.newLine
|
||||
}
|
||||
if len(sourceFile.Text()) == 0 || !stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) {
|
||||
options.Suffix = t.newLine
|
||||
}
|
||||
if blankLineBetween {
|
||||
options.Suffix += t.newLine
|
||||
}
|
||||
|
||||
if len(insert) == 1 {
|
||||
t.InsertNodeAt(sourceFile, core.TextPos(pos), insert[0], options)
|
||||
} else {
|
||||
t.InsertNodesAt(sourceFile, core.TextPos(pos), insert, options)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) InsertMemberAtStart(sourceFile *ast.SourceFile, node *ast.Node, newElement *ast.Node) {
|
||||
t.insertNodeAtStartWorker(sourceFile, node, newElement)
|
||||
}
|
||||
|
||||
func (t *Tracker) insertNodeAtStartWorker(sourceFile *ast.SourceFile, node *ast.Node, newElement *ast.Node) {
|
||||
indentation := t.tryComputeIndentationFromExistingMembers(sourceFile, node)
|
||||
if indentation < 0 {
|
||||
indentation = t.tryComputeIndentationForNewMember(sourceFile, node)
|
||||
}
|
||||
|
||||
members := getMembersOrProperties(node)
|
||||
if members == nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.InsertNodeAt(sourceFile, core.TextPos(members.Pos()), newElement, t.getInsertNodeAtStartInsertOptions(sourceFile, node, indentation))
|
||||
}
|
||||
|
||||
func (t *Tracker) tryComputeIndentationForNewMember(sourceFile *ast.SourceFile, node *ast.Node) int {
|
||||
nodeStart := astnav.GetStartOfNode(node, sourceFile, false)
|
||||
lineStart := format.GetLineStartPositionForPosition(nodeStart, sourceFile)
|
||||
|
||||
tabSize := t.formatSettings.TabSize
|
||||
if tabSize <= 0 {
|
||||
tabSize = 4
|
||||
}
|
||||
|
||||
indentSize := t.formatSettings.IndentSize
|
||||
if indentSize <= 0 {
|
||||
indentSize = 4
|
||||
}
|
||||
return max(findIndentationColumn(sourceFile.Text(), lineStart, nodeStart, tabSize), 0) + indentSize
|
||||
}
|
||||
|
||||
func (t *Tracker) tryComputeIndentationFromExistingMembers(sourceFile *ast.SourceFile, node *ast.Node) int {
|
||||
members := getMembersOrProperties(node)
|
||||
if members == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
indentation := -1
|
||||
text := sourceFile.Text()
|
||||
tabSize := t.formatSettings.TabSize
|
||||
last := node
|
||||
|
||||
if tabSize <= 0 {
|
||||
tabSize = 4
|
||||
}
|
||||
|
||||
for _, member := range members.Nodes {
|
||||
if member == nil {
|
||||
continue
|
||||
}
|
||||
if printer.RangeStartPositionsAreOnSameLine(last.Loc, member.Loc, sourceFile) {
|
||||
return -1
|
||||
}
|
||||
|
||||
memberStart := astnav.GetStartOfNode(member, sourceFile, false)
|
||||
lineStart := format.GetLineStartPositionForPosition(memberStart, sourceFile)
|
||||
column := findIndentationColumn(text, lineStart, memberStart, tabSize)
|
||||
if column < 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
if indentation >= 0 {
|
||||
if indentation != column {
|
||||
return -1
|
||||
}
|
||||
last = member
|
||||
continue
|
||||
}
|
||||
|
||||
indentation = column
|
||||
last = member
|
||||
}
|
||||
|
||||
return indentation
|
||||
}
|
||||
|
||||
func (t *Tracker) getInsertNodeAfterOptions(sourceFile *ast.SourceFile, node *ast.Node) NodeOptions {
|
||||
newLineChar := t.newLine
|
||||
var options NodeOptions
|
||||
switch node.Kind {
|
||||
case ast.KindParameter:
|
||||
// default opts
|
||||
options = NodeOptions{}
|
||||
case ast.KindClassDeclaration, ast.KindModuleDeclaration:
|
||||
options = NodeOptions{Prefix: newLineChar, Suffix: newLineChar}
|
||||
|
||||
case ast.KindVariableDeclaration, ast.KindStringLiteral, ast.KindIdentifier:
|
||||
options = NodeOptions{Prefix: ", "}
|
||||
|
||||
case ast.KindPropertyAssignment:
|
||||
options = NodeOptions{Suffix: "," + newLineChar}
|
||||
|
||||
case ast.KindExportKeyword:
|
||||
options = NodeOptions{Prefix: " "}
|
||||
|
||||
default:
|
||||
if !(ast.IsStatement(node) || ast.IsClassOrTypeElement(node)) {
|
||||
// Else we haven't handled this kind of node yet -- add it
|
||||
panic("unimplemented node type " + node.Kind.String() + " in changeTracker.getInsertNodeAfterOptions")
|
||||
}
|
||||
options = NodeOptions{Suffix: newLineChar}
|
||||
}
|
||||
if node.End() == sourceFile.End() && ast.IsStatement(node) {
|
||||
options.Prefix = t.newLine + options.Prefix
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func (t *Tracker) getOptionsForInsertNodeBefore(before *ast.Node, inserted *ast.Node, blankLineBetween bool) NodeOptions {
|
||||
if ast.IsStatement(before) || ast.IsClassOrTypeElement(before) {
|
||||
if blankLineBetween {
|
||||
return NodeOptions{Suffix: t.newLine + t.newLine}
|
||||
}
|
||||
return NodeOptions{Suffix: t.newLine}
|
||||
} else if before.Kind == ast.KindVariableDeclaration {
|
||||
// insert `x = 1, ` into `const x = 1, y = 2;
|
||||
return NodeOptions{Suffix: ", "}
|
||||
} else if before.Kind == ast.KindParameter {
|
||||
if inserted.Kind == ast.KindParameter {
|
||||
return NodeOptions{Suffix: ", "}
|
||||
}
|
||||
return NodeOptions{}
|
||||
} else if (before.Kind == ast.KindStringLiteral && before.Parent != nil && before.Parent.Kind == ast.KindImportDeclaration) || before.Kind == ast.KindNamedImports {
|
||||
return NodeOptions{Suffix: ", "}
|
||||
} else if before.Kind == ast.KindImportSpecifier {
|
||||
suffix := ","
|
||||
if blankLineBetween {
|
||||
suffix += t.newLine
|
||||
} else {
|
||||
suffix += " "
|
||||
}
|
||||
return NodeOptions{Suffix: suffix}
|
||||
}
|
||||
// We haven't handled this kind of node yet -- add it
|
||||
panic("unimplemented node type " + before.Kind.String() + " in changeTracker.getOptionsForInsertNodeBefore")
|
||||
}
|
||||
|
||||
func (t *Tracker) getInsertNodeAtStartInsertOptions(sourceFile *ast.SourceFile, node *ast.Node, indentation int) NodeOptions {
|
||||
state := t.nodesWithInsertionsAtStart[node]
|
||||
hasPreviousInsertion := state != nil
|
||||
if state == nil {
|
||||
state = &nodesInsertedAtStartState{
|
||||
node: node,
|
||||
sourceFile: sourceFile,
|
||||
}
|
||||
t.nodesWithInsertionsAtStart[node] = state
|
||||
}
|
||||
|
||||
members := getMembersOrProperties(node)
|
||||
isObjectLiteral := ast.IsObjectLiteralExpression(node)
|
||||
isJSON := ast.IsJsonSourceFile(sourceFile)
|
||||
|
||||
hasMembers := members != nil && len(members.Nodes) > 0
|
||||
|
||||
insertTrailingComma := isObjectLiteral && (hasMembers || !isJSON)
|
||||
insertLeadingComma := isObjectLiteral && isJSON && !hasMembers && hasPreviousInsertion
|
||||
|
||||
suffix := ""
|
||||
if insertTrailingComma {
|
||||
suffix = ","
|
||||
} else if ast.IsInterfaceDeclaration(node) && !hasMembers {
|
||||
suffix = ";"
|
||||
}
|
||||
|
||||
prefix := t.newLine
|
||||
if insertLeadingComma {
|
||||
prefix = "," + prefix
|
||||
}
|
||||
|
||||
return NodeOptions{indentation: &indentation, Prefix: prefix, Suffix: suffix}
|
||||
}
|
||||
|
||||
func (t *Tracker) finishNodesWithInsertionsAtStart() {
|
||||
for _, state := range t.nodesWithInsertionsAtStart {
|
||||
if state == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
openBrace := astnav.FindChildOfKind(state.node, ast.KindOpenBraceToken, state.sourceFile)
|
||||
if openBrace == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
closeBrace := astnav.FindChildOfKind(state.node, ast.KindCloseBraceToken, state.sourceFile)
|
||||
if closeBrace == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
members := getMembersOrProperties(state.node)
|
||||
isEmpty := members == nil || len(members.Nodes) == 0
|
||||
isSingleLine := positionsAreOnSameLine(openBrace.End(), closeBrace.End(), state.sourceFile)
|
||||
|
||||
if isEmpty && isSingleLine && openBrace.End() != closeBrace.End()-1 {
|
||||
t.DeleteRange(state.sourceFile, core.NewTextRange(openBrace.End(), closeBrace.End()-1))
|
||||
}
|
||||
|
||||
if isSingleLine {
|
||||
t.InsertText(state.sourceFile, t.converters.PositionToLineAndCharacter(state.sourceFile, core.TextPos(closeBrace.End()-1)), t.newLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getMembersOrProperties(node *ast.Node) *ast.NodeList {
|
||||
if ast.IsObjectLiteralExpression(node) {
|
||||
return node.PropertyList()
|
||||
}
|
||||
return node.MemberList()
|
||||
}
|
||||
|
||||
func rangeContainsRangeExclusive(outer *ast.Node, inner *ast.Node) bool {
|
||||
return outer.Pos() < inner.Pos() && inner.End() < outer.End()
|
||||
}
|
||||
|
||||
func isSeparator(node *ast.Node, candidate *ast.Node) bool {
|
||||
return candidate != nil && node.Parent != nil && (candidate.Kind == ast.KindCommaToken || (candidate.Kind == ast.KindSemicolonToken && node.Parent.Kind == ast.KindObjectLiteralExpression))
|
||||
}
|
||||
|
||||
func findIndentationColumn(text string, lineStart, memberStart, tabSize int) int {
|
||||
column := 0
|
||||
|
||||
for i := lineStart; i < memberStart && i < len(text); i++ {
|
||||
ch := rune(text[i])
|
||||
|
||||
if stringutil.IsLineBreak(ch) {
|
||||
return -1
|
||||
}
|
||||
if stringutil.IsWhiteSpaceSingleLine(ch) {
|
||||
column = advanceIndentationColumn(column, ch, tabSize)
|
||||
continue
|
||||
}
|
||||
return column
|
||||
}
|
||||
|
||||
return column
|
||||
}
|
||||
|
||||
func advanceIndentationColumn(column int, ch rune, tabSize int) int {
|
||||
if ch == '\t' {
|
||||
return column + tabSize - (column % tabSize)
|
||||
}
|
||||
return column + 1
|
||||
}
|
||||
402
tools/tsgo/internal/ls/change/trackerimpl.go
Normal file
402
tools/tsgo/internal/ls/change/trackerimpl.go
Normal file
@@ -0,0 +1,402 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/format"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
func (t *Tracker) getTextChangesFromChanges() map[string][]*lsproto.TextEdit {
|
||||
changes := map[string][]*lsproto.TextEdit{}
|
||||
for sourceFile, changesInFile := range t.changes.M {
|
||||
// order changes by start position
|
||||
// If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa.
|
||||
slices.SortStableFunc(changesInFile, func(a, b *trackerEdit) int { return lsproto.CompareRanges(a.Range, b.Range) })
|
||||
// verify that change intervals do not overlap, except possibly at end points.
|
||||
for i := range len(changesInFile) - 1 {
|
||||
if lsproto.ComparePositions(changesInFile[i].Range.End, changesInFile[i+1].Range.Start) > 0 {
|
||||
// assert change[i].End <= change[i + 1].Start
|
||||
panic(fmt.Sprintf("changes overlap: %v and %v", changesInFile[i].Range, changesInFile[i+1].Range))
|
||||
}
|
||||
}
|
||||
|
||||
textChanges := core.MapNonNil(changesInFile, func(change *trackerEdit) *lsproto.TextEdit {
|
||||
// !!! targetSourceFile
|
||||
|
||||
newText := t.computeNewText(change, sourceFile, sourceFile)
|
||||
// span := createTextSpanFromRange(c.Range)
|
||||
// !!!
|
||||
// Filter out redundant changes.
|
||||
// if (span.length == newText.length && stringContainsAt(targetSourceFile.text, newText, span.start)) { return nil }
|
||||
|
||||
return &lsproto.TextEdit{
|
||||
NewText: newText,
|
||||
Range: change.Range,
|
||||
}
|
||||
})
|
||||
|
||||
if len(textChanges) > 0 {
|
||||
changes[sourceFile.FileName()] = textChanges
|
||||
}
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func (t *Tracker) computeNewText(change *trackerEdit, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile) string {
|
||||
switch change.kind {
|
||||
case trackerEditKindRemove:
|
||||
return ""
|
||||
case trackerEditKindText:
|
||||
return change.NewText
|
||||
}
|
||||
|
||||
pos := int(t.converters.LineAndCharacterToPosition(sourceFile, change.Range.Start))
|
||||
formatNode := func(n *ast.Node) string {
|
||||
return t.getFormattedTextOfNode(n, targetSourceFile, sourceFile, pos, change.options)
|
||||
}
|
||||
|
||||
var text string
|
||||
switch change.kind {
|
||||
|
||||
case trackerEditKindReplaceWithMultipleNodes:
|
||||
if change.options.joiner == "" {
|
||||
change.options.joiner = t.newLine
|
||||
}
|
||||
text = strings.Join(core.Map(change.nodes, func(n *ast.Node) string { return strings.TrimSuffix(formatNode(n), t.newLine) }), change.options.joiner)
|
||||
case trackerEditKindReplaceWithSingleNode:
|
||||
text = formatNode(change.Node)
|
||||
default:
|
||||
panic(fmt.Sprintf("change kind %d should have been handled earlier", change.kind))
|
||||
}
|
||||
// strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line
|
||||
noIndent := text
|
||||
if !(change.options.indentation != nil || format.GetLineStartPositionForPosition(pos, targetSourceFile) == pos) {
|
||||
noIndent = strings.TrimLeftFunc(text, unicode.IsSpace)
|
||||
}
|
||||
return change.options.Prefix + noIndent + core.IfElse(strings.HasSuffix(noIndent, change.options.Suffix), "", change.options.Suffix)
|
||||
}
|
||||
|
||||
/** Note: this may mutate `nodeIn`. */
|
||||
func (t *Tracker) getFormattedTextOfNode(nodeIn *ast.Node, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile, pos int, options NodeOptions) string {
|
||||
text, sourceFileLike := t.getNonformattedText(nodeIn, targetSourceFile)
|
||||
// !!! if (validate) validate(node, text);
|
||||
formatOptions := getFormatCodeSettingsForWriting(t.formatSettings, targetSourceFile)
|
||||
|
||||
var initialIndentation, delta int
|
||||
if options.indentation == nil {
|
||||
initialIndentation = format.GetIndentation(pos, sourceFile, formatOptions, options.Prefix == t.newLine || format.GetLineStartPositionForPosition(pos, targetSourceFile) == pos)
|
||||
} else {
|
||||
initialIndentation = *options.indentation
|
||||
}
|
||||
|
||||
if options.delta != nil {
|
||||
delta = *options.delta
|
||||
} else if formatOptions.IndentSize != 0 && format.ShouldIndentChildNode(formatOptions, nodeIn, nil, nil) {
|
||||
delta = formatOptions.IndentSize
|
||||
}
|
||||
|
||||
changes := format.FormatNodeGivenIndentation(t.ctx, sourceFileLike, sourceFileLike.AsSourceFile(), targetSourceFile.LanguageVariant, initialIndentation, delta)
|
||||
return core.ApplyBulkEdits(text, changes)
|
||||
}
|
||||
|
||||
func getFormatCodeSettingsForWriting(options lsutil.FormatCodeSettings, sourceFile *ast.SourceFile) lsutil.FormatCodeSettings {
|
||||
shouldAutoDetectSemicolonPreference := options.Semicolons == lsutil.SemicolonPreferenceIgnore
|
||||
shouldRemoveSemicolons := options.Semicolons == lsutil.SemicolonPreferenceRemove || shouldAutoDetectSemicolonPreference && !lsutil.ProbablyUsesSemicolons(sourceFile)
|
||||
if shouldRemoveSemicolons {
|
||||
options.Semicolons = lsutil.SemicolonPreferenceRemove
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func (t *Tracker) getNonformattedText(node *ast.Node, sourceFile *ast.SourceFile) (string, *ast.Node) {
|
||||
writer := printer.NewChangeTrackerWriter(t.newLine, t.formatSettings.IndentSize)
|
||||
printer.NewPrinter(
|
||||
printer.PrinterOptions{
|
||||
NewLine: core.GetNewLineKind(t.newLine),
|
||||
NeverAsciiEscape: true,
|
||||
PreserveSourceNewlines: true,
|
||||
TerminateUnterminatedLiterals: true,
|
||||
},
|
||||
writer.GetPrintHandlers(),
|
||||
t.EmitContext,
|
||||
).Write(node, sourceFile, writer, nil)
|
||||
|
||||
text := writer.String()
|
||||
text = strings.TrimSuffix(text, t.newLine)
|
||||
|
||||
nodeOut := writer.AssignPositionsToNode(node, t.NodeFactory)
|
||||
eofToken := t.Factory.NewToken(ast.KindEndOfFile)
|
||||
nodeList := t.Factory.NewNodeList([]*ast.Node{nodeOut})
|
||||
nodeList.Loc = nodeOut.Loc
|
||||
eofToken.Loc = core.NewTextRange(nodeOut.End(), nodeOut.End())
|
||||
sourceFileLike := t.Factory.NewSourceFile(
|
||||
ast.SourceFileParseOptions{FileName: sourceFile.FileName(), Path: sourceFile.Path()},
|
||||
text,
|
||||
nodeList,
|
||||
eofToken,
|
||||
)
|
||||
sourceFileLike.ForEachChild(func(child *ast.Node) bool {
|
||||
child.Parent = sourceFileLike
|
||||
return true
|
||||
})
|
||||
sourceFileLike.Loc = nodeOut.Loc
|
||||
return text, sourceFileLike
|
||||
}
|
||||
|
||||
// method on the changeTracker because use of converters
|
||||
// GetAdjustedRange computes the adjusted range for a node in a source file, accounting for trivia.
|
||||
func (t *Tracker) GetAdjustedRange(sourceFile *ast.SourceFile, startNode *ast.Node, endNode *ast.Node, leadingOption LeadingTriviaOption, trailingOption TrailingTriviaOption) lsproto.Range {
|
||||
return t.converters.ToLSPRange(
|
||||
sourceFile,
|
||||
core.NewTextRange(
|
||||
t.getAdjustedStartPosition(sourceFile, startNode, leadingOption, false),
|
||||
t.getAdjustedEndPosition(sourceFile, endNode, trailingOption),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// method on the changeTracker because use of converters
|
||||
func (t *Tracker) getAdjustedStartPosition(sourceFile *ast.SourceFile, node *ast.Node, leadingOption LeadingTriviaOption, hasTrailingComment bool) int {
|
||||
if leadingOption == LeadingTriviaOptionJSDoc {
|
||||
if JSDocComments := parser.GetJSDocCommentRanges(t.NodeFactory, nil, node, sourceFile.Text()); len(JSDocComments) > 0 {
|
||||
return format.GetLineStartPositionForPosition(JSDocComments[0].Pos(), sourceFile)
|
||||
}
|
||||
}
|
||||
|
||||
start := astnav.GetStartOfNode(node, sourceFile, false)
|
||||
startOfLinePos := format.GetLineStartPositionForPosition(start, sourceFile)
|
||||
|
||||
switch leadingOption {
|
||||
case LeadingTriviaOptionExclude:
|
||||
return start
|
||||
case LeadingTriviaOptionStartLine:
|
||||
if node.Loc.ContainsInclusive(startOfLinePos) {
|
||||
return startOfLinePos
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
fullStart := node.Pos()
|
||||
if fullStart == start {
|
||||
return start
|
||||
}
|
||||
lineStarts := sourceFile.ECMALineMap()
|
||||
fullStartLineIndex := scanner.ComputeLineOfPosition(lineStarts, fullStart)
|
||||
fullStartLinePos := int(lineStarts[fullStartLineIndex])
|
||||
if startOfLinePos == fullStartLinePos {
|
||||
// full start and start of the node are on the same line
|
||||
// a, b;
|
||||
// ^ ^
|
||||
// | start
|
||||
// fullstart
|
||||
// when b is replaced - we usually want to keep the leading trvia
|
||||
// when b is deleted - we delete it
|
||||
if leadingOption == LeadingTriviaOptionIncludeAll {
|
||||
return fullStart
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
// if node has a trailing comments, use comment end position as the text has already been included.
|
||||
if hasTrailingComment {
|
||||
// Check first for leading comments as if the node is the first import, we want to exclude the trivia;
|
||||
// otherwise we get the trailing comments.
|
||||
comments := slices.Collect(scanner.GetLeadingCommentRanges(t.NodeFactory, sourceFile.Text(), fullStart))
|
||||
if len(comments) == 0 {
|
||||
comments = slices.Collect(scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), fullStart))
|
||||
}
|
||||
if len(comments) > 0 {
|
||||
return scanner.SkipTriviaEx(sourceFile.Text(), comments[0].End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true})
|
||||
}
|
||||
}
|
||||
|
||||
// get start position of the line following the line that contains fullstart position
|
||||
// (but only if the fullstart isn't the very beginning of the file)
|
||||
nextLineStart := core.IfElse(fullStart > 0, 1, 0)
|
||||
adjustedStartPosition := int(lineStarts[fullStartLineIndex+nextLineStart])
|
||||
// skip whitespaces/newlines
|
||||
adjustedStartPosition = scanner.SkipTriviaEx(sourceFile.Text(), adjustedStartPosition, &scanner.SkipTriviaOptions{StopAtComments: true})
|
||||
return int(lineStarts[scanner.ComputeLineOfPosition(lineStarts, adjustedStartPosition)])
|
||||
}
|
||||
|
||||
// method on the changeTracker because of converters
|
||||
// Return the end position of a multiline comment of it is on another line; otherwise returns `undefined`;
|
||||
func (t *Tracker) getEndPositionOfMultilineTrailingComment(sourceFile *ast.SourceFile, node *ast.Node, trailingOpt TrailingTriviaOption) int {
|
||||
if trailingOpt == TrailingTriviaOptionInclude {
|
||||
// If the trailing comment is a multiline comment that extends to the next lines,
|
||||
// return the end of the comment and track it for the next nodes to adjust.
|
||||
lineStarts := sourceFile.ECMALineMap()
|
||||
nodeEndLine := scanner.ComputeLineOfPosition(lineStarts, node.End())
|
||||
for comment := range scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End()) {
|
||||
// Single line can break the loop as trivia will only be this line.
|
||||
// Comments on subsequent lines are also ignored.
|
||||
if comment.Kind == ast.KindSingleLineCommentTrivia || scanner.ComputeLineOfPosition(lineStarts, comment.Pos()) > nodeEndLine {
|
||||
break
|
||||
}
|
||||
|
||||
// Get the end line of the comment and compare against the end line of the node.
|
||||
// If the comment end line position and the multiline comment extends to multiple lines,
|
||||
// then is safe to return the end position.
|
||||
if commentEndLine := scanner.ComputeLineOfPosition(lineStarts, comment.End()); commentEndLine > nodeEndLine {
|
||||
return scanner.SkipTriviaEx(sourceFile.Text(), comment.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// method on the changeTracker because of converters
|
||||
func (t *Tracker) getAdjustedEndPosition(sourceFile *ast.SourceFile, node *ast.Node, TrailingTriviaOption TrailingTriviaOption) int {
|
||||
if TrailingTriviaOption == TrailingTriviaOptionExclude {
|
||||
return node.End()
|
||||
}
|
||||
if TrailingTriviaOption == TrailingTriviaOptionExcludeWhitespace {
|
||||
if comments := slices.AppendSeq(
|
||||
slices.Collect(scanner.GetTrailingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End())),
|
||||
scanner.GetLeadingCommentRanges(t.NodeFactory, sourceFile.Text(), node.End()),
|
||||
); len(comments) > 0 {
|
||||
if realEnd := comments[len(comments)-1].End(); realEnd != 0 {
|
||||
return realEnd
|
||||
}
|
||||
}
|
||||
return node.End()
|
||||
}
|
||||
|
||||
if multilineEndPosition := t.getEndPositionOfMultilineTrailingComment(sourceFile, node, TrailingTriviaOption); multilineEndPosition != 0 {
|
||||
return multilineEndPosition
|
||||
}
|
||||
|
||||
newEnd := scanner.SkipTriviaEx(sourceFile.Text(), node.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true})
|
||||
|
||||
if newEnd != node.End() && (TrailingTriviaOption == TrailingTriviaOptionInclude || stringutil.IsLineBreak(rune(sourceFile.Text()[newEnd-1]))) {
|
||||
return newEnd
|
||||
}
|
||||
return node.End()
|
||||
}
|
||||
|
||||
// ============= utilities =============
|
||||
|
||||
func hasCommentsBeforeLineBreak(text string, start int) bool {
|
||||
for _, ch := range []rune(text[start:]) {
|
||||
if !stringutil.IsWhiteSpaceSingleLine(ch) {
|
||||
return ch == '/'
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func needSemicolonBetween(a, b *ast.Node) bool {
|
||||
return (ast.IsPropertySignatureDeclaration(a) || ast.IsPropertyDeclaration(a)) &&
|
||||
ast.IsClassOrTypeElement(b) &&
|
||||
b.Name().Kind == ast.KindComputedPropertyName ||
|
||||
ast.IsStatementButNotDeclaration(a) &&
|
||||
ast.IsStatementButNotDeclaration(b) // TODO: only if b would start with a `(` or `[`
|
||||
}
|
||||
|
||||
func (t *Tracker) getInsertionPositionAtSourceFileTop(sourceFile *ast.SourceFile) int {
|
||||
var lastPrologue *ast.Node
|
||||
for _, node := range sourceFile.Statements.Nodes {
|
||||
if ast.IsPrologueDirective(node) {
|
||||
lastPrologue = node
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
position := 0
|
||||
text := sourceFile.Text()
|
||||
advancePastLineBreak := func() {
|
||||
if position >= len(text) {
|
||||
return
|
||||
}
|
||||
if char := rune(text[position]); stringutil.IsLineBreak(char) {
|
||||
position++
|
||||
if position < len(text) && char == '\r' && rune(text[position]) == '\n' {
|
||||
position++
|
||||
}
|
||||
}
|
||||
}
|
||||
if lastPrologue != nil {
|
||||
position = lastPrologue.End()
|
||||
advancePastLineBreak()
|
||||
return position
|
||||
}
|
||||
|
||||
shebang := scanner.GetShebang(text)
|
||||
if shebang != "" {
|
||||
position = len(shebang)
|
||||
advancePastLineBreak()
|
||||
}
|
||||
|
||||
ranges := slices.Collect(scanner.GetLeadingCommentRanges(t.NodeFactory, text, position))
|
||||
if len(ranges) == 0 {
|
||||
return position
|
||||
}
|
||||
// Find the first attached comment to the first node and add before it
|
||||
var lastComment *ast.CommentRange
|
||||
pinnedOrTripleSlash := false
|
||||
firstNodeLine := -1
|
||||
|
||||
lenStatements := len(sourceFile.Statements.Nodes)
|
||||
lineMap := sourceFile.ECMALineMap()
|
||||
for _, r := range ranges {
|
||||
if r.Kind == ast.KindMultiLineCommentTrivia {
|
||||
if printer.IsPinnedComment(text, r) {
|
||||
lastComment = &r
|
||||
pinnedOrTripleSlash = true
|
||||
continue
|
||||
}
|
||||
} else if printer.IsRecognizedTripleSlashComment(text, r) {
|
||||
lastComment = &r
|
||||
pinnedOrTripleSlash = true
|
||||
continue
|
||||
}
|
||||
|
||||
if lastComment != nil {
|
||||
// Always insert after pinned or triple slash comments
|
||||
if pinnedOrTripleSlash {
|
||||
break
|
||||
}
|
||||
|
||||
// There was a blank line between the last comment and this comment.
|
||||
// This comment is not part of the copyright comments
|
||||
commentLine := scanner.ComputeLineOfPosition(lineMap, r.Pos())
|
||||
lastCommentEndLine := scanner.ComputeLineOfPosition(lineMap, lastComment.End())
|
||||
if commentLine >= lastCommentEndLine+2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lenStatements > 0 {
|
||||
if firstNodeLine == -1 {
|
||||
firstNodeLine = scanner.ComputeLineOfPosition(lineMap, astnav.GetStartOfNode(sourceFile.Statements.Nodes[0], sourceFile, false))
|
||||
}
|
||||
commentEndLine := scanner.ComputeLineOfPosition(lineMap, r.End())
|
||||
if firstNodeLine < commentEndLine+2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
lastComment = &r
|
||||
pinnedOrTripleSlash = false
|
||||
}
|
||||
|
||||
if lastComment != nil {
|
||||
position = lastComment.End()
|
||||
advancePastLineBreak()
|
||||
}
|
||||
return position
|
||||
}
|
||||
399
tools/tsgo/internal/ls/codeactions.go
Normal file
399
tools/tsgo/internal/ls/codeactions.go
Normal file
@@ -0,0 +1,399 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"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/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
)
|
||||
|
||||
// CodeFixProvider represents a provider for a specific type of code fix
|
||||
type CodeFixProvider struct {
|
||||
ErrorCodes []int32
|
||||
GetCodeActions func(ctx context.Context, fixContext *CodeFixContext) ([]*CodeAction, error)
|
||||
FixIds []string
|
||||
GetAllCodeActions func(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error)
|
||||
}
|
||||
|
||||
// CodeFixContext contains the context needed to generate code fixes
|
||||
type CodeFixContext struct {
|
||||
SourceFile *ast.SourceFile
|
||||
Span core.TextRange
|
||||
ErrorCode int32
|
||||
Program *compiler.Program
|
||||
LS *LanguageService
|
||||
Diagnostic *lsproto.Diagnostic
|
||||
Params *lsproto.CodeActionParams
|
||||
}
|
||||
|
||||
// CodeAction represents a single code action fix
|
||||
type CodeAction struct {
|
||||
Description string
|
||||
Changes []*lsproto.TextEdit
|
||||
FixID string
|
||||
FixAllDescription string
|
||||
}
|
||||
|
||||
// Compare defines a total ordering for CodeAction values, comparing description
|
||||
// then text edits lexicographically. Used with slices.BinarySearchFunc.
|
||||
func (a *CodeAction) Compare(b *CodeAction) int {
|
||||
if c := strings.Compare(a.Description, b.Description); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := cmp.Compare(len(a.Changes), len(b.Changes)); c != 0 {
|
||||
return c
|
||||
}
|
||||
for i, edit := range a.Changes {
|
||||
if c := edit.Compare(b.Changes[i]); c != 0 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// CombinedCodeActions represents combined code actions for fix-all scenarios
|
||||
type CombinedCodeActions struct {
|
||||
Description string
|
||||
Changes []*lsproto.TextEdit
|
||||
}
|
||||
|
||||
// codeFixProviders is the list of all registered code fix providers
|
||||
var codeFixProviders = []*CodeFixProvider{
|
||||
ImportFixProvider,
|
||||
IsolatedDeclarationsFixProvider,
|
||||
FixClassIncorrectlyImplementsInterfaceProvider,
|
||||
// Add more code fix providers here as they are implemented
|
||||
}
|
||||
|
||||
// ProvideCodeActions returns code actions for the given range and context
|
||||
func (l *LanguageService) ProvideCodeActions(ctx context.Context, params *lsproto.CodeActionParams) (lsproto.CodeActionResponse, error) {
|
||||
program, file := l.getProgramAndFile(params.TextDocument.Uri)
|
||||
|
||||
var actions []lsproto.CommandOrCodeAction
|
||||
|
||||
if params.Context != nil && params.Context.Only != nil {
|
||||
for _, kind := range *params.Context.Only {
|
||||
matchingKinds := getOrganizeImportsActionsForKind(kind)
|
||||
for _, matchingKind := range matchingKinds {
|
||||
organizeAction := l.createOrganizeImportsAction(ctx, program, file, matchingKind)
|
||||
actions = append(actions, *organizeAction)
|
||||
}
|
||||
|
||||
if isFixAllKind(kind) {
|
||||
fixAllAction, err := l.createFixAllAction(ctx, program, file, params.TextDocument.Uri)
|
||||
if err != nil {
|
||||
return lsproto.CodeActionResponse{}, err
|
||||
}
|
||||
if fixAllAction != nil {
|
||||
actions = append(actions, *fixAllAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if params.Context != nil && params.Context.Diagnostics != nil && wantsQuickFixes(params.Context.Only) {
|
||||
fixIdSeen := make(map[string]*CodeFixProvider)
|
||||
|
||||
var seen []*CodeAction // sorted for binary search dedup, dedup across all diagnostics and providers so if multiple diags produce the same codefix, only one is returned
|
||||
|
||||
for _, diag := range params.Context.Diagnostics {
|
||||
if diag.Code == nil || diag.Code.Integer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
errorCode := *diag.Code.Integer
|
||||
|
||||
for _, provider := range codeFixProviders {
|
||||
if !containsErrorCode(provider.ErrorCodes, errorCode) {
|
||||
continue
|
||||
}
|
||||
|
||||
position := l.converters.LineAndCharacterToPosition(file, diag.Range.Start)
|
||||
endPosition := l.converters.LineAndCharacterToPosition(file, diag.Range.End)
|
||||
fixContext := &CodeFixContext{
|
||||
SourceFile: file,
|
||||
Span: core.NewTextRange(int(position), int(endPosition)),
|
||||
ErrorCode: errorCode,
|
||||
Program: program,
|
||||
LS: l,
|
||||
Diagnostic: diag,
|
||||
Params: params,
|
||||
}
|
||||
|
||||
providerActions, err := provider.GetCodeActions(ctx, fixContext)
|
||||
if err != nil {
|
||||
return lsproto.CodeActionResponse{}, err
|
||||
}
|
||||
for _, action := range providerActions {
|
||||
i, found := slices.BinarySearchFunc(seen, action, (*CodeAction).Compare)
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
seen = slices.Insert(seen, i, action)
|
||||
actions = append(actions, convertToLSPCodeAction(action, diag, params.TextDocument.Uri))
|
||||
if action.FixID != "" {
|
||||
fixIdSeen[action.FixID] = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fixAllActions, err := l.getFixAllQuickFixes(ctx, program, file, params.TextDocument.Uri, fixIdSeen)
|
||||
if err != nil {
|
||||
return lsproto.CodeActionResponse{}, err
|
||||
}
|
||||
actions = append(actions, fixAllActions...)
|
||||
}
|
||||
|
||||
return lsproto.CommandOrCodeActionArrayOrNull{CommandOrCodeActionArray: &actions}, nil
|
||||
}
|
||||
|
||||
// getFixAllQuickFixes returns per-provider "Fix all in file" quickfix entries for providers
|
||||
// that matched at least 2 diagnostics in the full file.
|
||||
func (l *LanguageService) getFixAllQuickFixes(
|
||||
ctx context.Context,
|
||||
program *compiler.Program,
|
||||
file *ast.SourceFile,
|
||||
uri lsproto.DocumentUri,
|
||||
fixIdSeen map[string]*CodeFixProvider,
|
||||
) ([]lsproto.CommandOrCodeAction, error) {
|
||||
var actions []lsproto.CommandOrCodeAction
|
||||
|
||||
// Deduplicate providers; multiple fixIds may map to the same provider.
|
||||
var seen collections.Set[*CodeFixProvider]
|
||||
for _, provider := range fixIdSeen {
|
||||
if seen.Has(provider) {
|
||||
continue
|
||||
}
|
||||
seen.Add(provider)
|
||||
|
||||
if provider.GetAllCodeActions == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !hasMultipleFixableDiagnostics(ctx, program, file, provider.ErrorCodes) {
|
||||
continue
|
||||
}
|
||||
|
||||
fixContext := &CodeFixContext{
|
||||
SourceFile: file,
|
||||
Program: program,
|
||||
LS: l,
|
||||
}
|
||||
combined, err := provider.GetAllCodeActions(ctx, fixContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if combined != nil && len(combined.Changes) > 0 {
|
||||
kind := lsproto.CodeActionKindQuickFix
|
||||
changes := map[lsproto.DocumentUri][]*lsproto.TextEdit{
|
||||
uri: combined.Changes,
|
||||
}
|
||||
actions = append(actions, lsproto.CommandOrCodeAction{
|
||||
CodeAction: &lsproto.CodeAction{
|
||||
Title: combined.Description,
|
||||
Kind: &kind,
|
||||
Edit: &lsproto.WorkspaceEdit{Changes: &changes},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
// hasMultipleFixableDiagnostics returns true if the file has at least 2 diagnostics
|
||||
// matching the given error codes. Checks all diagnostic sources (semantic,
|
||||
// syntactic, suggestion, declaration) to match ProvideDiagnostics.
|
||||
func hasMultipleFixableDiagnostics(ctx context.Context, program *compiler.Program, file *ast.SourceFile, errorCodes []int32) bool {
|
||||
allDiags := getAllDiagnostics(ctx, program, file)
|
||||
count := 0
|
||||
for _, d := range allDiags {
|
||||
if containsErrorCode(errorCodes, d.Code()) {
|
||||
count++
|
||||
if count >= 2 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// codeActionKindContains returns true if the requested kind equals or is a
|
||||
// hierarchical parent of actionKind, using '.' as the separator. This matches
|
||||
// the semantics of VS Code's HierarchicalKind.contains.
|
||||
func codeActionKindContains(requestedKind, actionKind lsproto.CodeActionKind) bool {
|
||||
return requestedKind == actionKind ||
|
||||
requestedKind == "" ||
|
||||
strings.HasPrefix(string(actionKind), string(requestedKind)+".")
|
||||
}
|
||||
|
||||
// isFixAllKind returns true if the requested kind matches source.fixAll
|
||||
func isFixAllKind(kind lsproto.CodeActionKind) bool {
|
||||
return codeActionKindContains(kind, lsproto.CodeActionKindSourceFixAll)
|
||||
}
|
||||
|
||||
// wantsQuickFixes returns true if the Only filter is nil/empty (meaning all kinds are wanted)
|
||||
// or explicitly includes the quickfix kind.
|
||||
func wantsQuickFixes(only *[]lsproto.CodeActionKind) bool {
|
||||
if only == nil || len(*only) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, kind := range *only {
|
||||
if codeActionKindContains(kind, lsproto.CodeActionKindQuickFix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// createFixAllAction creates a source.fixAll code action that applies all auto-fixable
|
||||
// code fixes across the file.
|
||||
func (l *LanguageService) createFixAllAction(
|
||||
ctx context.Context,
|
||||
program *compiler.Program,
|
||||
file *ast.SourceFile,
|
||||
uri lsproto.DocumentUri,
|
||||
) (*lsproto.CommandOrCodeAction, error) {
|
||||
kind := lsproto.CodeActionKindSourceFixAll
|
||||
lspChanges := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
|
||||
|
||||
for _, provider := range codeFixProviders {
|
||||
if provider.GetAllCodeActions == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fixContext := &CodeFixContext{
|
||||
SourceFile: file,
|
||||
Program: program,
|
||||
LS: l,
|
||||
}
|
||||
|
||||
combined, err := provider.GetAllCodeActions(ctx, fixContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if combined != nil && len(combined.Changes) > 0 {
|
||||
lspChanges[uri] = append(lspChanges[uri], combined.Changes...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(lspChanges) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &lsproto.CommandOrCodeAction{
|
||||
CodeAction: &lsproto.CodeAction{
|
||||
Title: diagnostics.Fix_All.Localize(locale.FromContext(ctx)),
|
||||
Kind: &kind,
|
||||
Edit: &lsproto.WorkspaceEdit{Changes: &lspChanges},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getOrganizeImportsActionTitle returns the appropriate title for the given organize imports kind
|
||||
func getOrganizeImportsActionTitle(ctx context.Context, kind lsproto.CodeActionKind) string {
|
||||
loc := locale.FromContext(ctx)
|
||||
switch kind {
|
||||
case lsproto.CodeActionKindSourceRemoveUnusedImports:
|
||||
return diagnostics.Remove_Unused_Imports.Localize(loc)
|
||||
case lsproto.CodeActionKindSourceSortImports:
|
||||
return diagnostics.Sort_Imports.Localize(loc)
|
||||
default:
|
||||
return diagnostics.Organize_Imports.Localize(loc)
|
||||
}
|
||||
}
|
||||
|
||||
// getOrganizeImportsActionsForKind returns the organize imports code action kinds that should be
|
||||
// returned for the given requested kind.
|
||||
func getOrganizeImportsActionsForKind(requestedKind lsproto.CodeActionKind) []lsproto.CodeActionKind {
|
||||
organizeImportsKinds := []lsproto.CodeActionKind{
|
||||
lsproto.CodeActionKindSourceOrganizeImports,
|
||||
lsproto.CodeActionKindSourceRemoveUnusedImports,
|
||||
lsproto.CodeActionKindSourceSortImports,
|
||||
}
|
||||
|
||||
var result []lsproto.CodeActionKind
|
||||
for _, organizeKind := range organizeImportsKinds {
|
||||
if codeActionKindContains(requestedKind, organizeKind) {
|
||||
result = append(result, organizeKind)
|
||||
}
|
||||
}
|
||||
|
||||
if slices.Contains(result, requestedKind) {
|
||||
return []lsproto.CodeActionKind{requestedKind}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createOrganizeImportsAction creates the organize imports code action
|
||||
func (l *LanguageService) createOrganizeImportsAction(
|
||||
ctx context.Context,
|
||||
program *compiler.Program,
|
||||
file *ast.SourceFile,
|
||||
kind lsproto.CodeActionKind,
|
||||
) *lsproto.CommandOrCodeAction {
|
||||
title := getOrganizeImportsActionTitle(ctx, kind)
|
||||
changes := l.OrganizeImports(
|
||||
ctx,
|
||||
file,
|
||||
program,
|
||||
kind,
|
||||
)
|
||||
if len(changes) == 0 {
|
||||
return &lsproto.CommandOrCodeAction{
|
||||
CodeAction: &lsproto.CodeAction{
|
||||
Title: title,
|
||||
Kind: &kind,
|
||||
Edit: &lsproto.WorkspaceEdit{Changes: &map[lsproto.DocumentUri][]*lsproto.TextEdit{}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
lspChanges := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
|
||||
for fileName, edits := range changes {
|
||||
fileURI := lsconv.FileNameToDocumentURI(fileName)
|
||||
lspChanges[fileURI] = edits
|
||||
}
|
||||
|
||||
return &lsproto.CommandOrCodeAction{
|
||||
CodeAction: &lsproto.CodeAction{
|
||||
Title: title,
|
||||
Kind: &kind,
|
||||
Edit: &lsproto.WorkspaceEdit{Changes: &lspChanges},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// containsErrorCode checks if the error code is in the list
|
||||
func containsErrorCode(codes []int32, code int32) bool {
|
||||
return slices.Contains(codes, code)
|
||||
}
|
||||
|
||||
// convertToLSPCodeAction converts an internal CodeAction to an LSP CodeAction
|
||||
func convertToLSPCodeAction(action *CodeAction, diag *lsproto.Diagnostic, uri lsproto.DocumentUri) lsproto.CommandOrCodeAction {
|
||||
kind := lsproto.CodeActionKindQuickFix
|
||||
changes := map[lsproto.DocumentUri][]*lsproto.TextEdit{
|
||||
uri: action.Changes,
|
||||
}
|
||||
diagnostics := []*lsproto.Diagnostic{diag}
|
||||
|
||||
return lsproto.CommandOrCodeAction{
|
||||
CodeAction: &lsproto.CodeAction{
|
||||
Title: action.Description,
|
||||
Kind: &kind,
|
||||
Edit: &lsproto.WorkspaceEdit{Changes: &changes},
|
||||
Diagnostics: &diagnostics,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/ls/autoimport"
|
||||
"github.com/microsoft/typescript-go/internal/ls/change"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
const fixClassIncorrectlyImplementsInterfaceFixID = "fixClassIncorrectlyImplementsInterface"
|
||||
|
||||
var fixClassIncorrectlyImplementsInterfaceErrorCodes = []int32{
|
||||
diagnostics.Class_0_incorrectly_implements_interface_1.Code(),
|
||||
diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass.Code(),
|
||||
}
|
||||
|
||||
var FixClassIncorrectlyImplementsInterfaceProvider = &CodeFixProvider{
|
||||
ErrorCodes: fixClassIncorrectlyImplementsInterfaceErrorCodes,
|
||||
GetCodeActions: getCodeActionsToFixClassIncorrectlyImplementsInterface,
|
||||
FixIds: []string{fixClassIncorrectlyImplementsInterfaceFixID},
|
||||
GetAllCodeActions: getAllCodeActionsToFixClassIncorrectlyImplementsInterface,
|
||||
}
|
||||
|
||||
func getCodeActionsToFixClassIncorrectlyImplementsInterface(context context.Context, fixContext *CodeFixContext) ([]*CodeAction, error) {
|
||||
classDeclaration := getClass(fixContext.SourceFile, fixContext.Span)
|
||||
if classDeclaration == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
implementsTypes := ast.GetImplementsTypeNodes(classDeclaration)
|
||||
locale := locale.FromContext(context)
|
||||
|
||||
typeChecker, done := fixContext.Program.GetTypeCheckerForFile(context, fixContext.SourceFile)
|
||||
defer done()
|
||||
|
||||
var actions []*CodeAction
|
||||
for _, implementedTypeNode := range implementsTypes {
|
||||
changeTracker := change.NewTracker(context, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters)
|
||||
importAdder, err := createImportAdder(context, fixContext, typeChecker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addChanges(context, fixContext, changeTracker, importAdder, typeChecker, classDeclaration, implementedTypeNode)
|
||||
changes := getChanges(changeTracker, importAdder, fixContext.SourceFile)
|
||||
if len(changes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
actions = append(actions, &CodeAction{
|
||||
Description: diagnostics.Implement_interface_0.Localize(locale, scanner.GetTextOfNode(implementedTypeNode)),
|
||||
Changes: changes,
|
||||
FixID: fixClassIncorrectlyImplementsInterfaceFixID,
|
||||
FixAllDescription: diagnostics.Implement_all_unimplemented_interfaces.Localize(locale),
|
||||
})
|
||||
}
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
func getAllCodeActionsToFixClassIncorrectlyImplementsInterface(context context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) {
|
||||
typeChecker, done := fixContext.Program.GetTypeCheckerForFile(context, fixContext.SourceFile)
|
||||
defer done()
|
||||
|
||||
changeTracker := change.NewTracker(context, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters)
|
||||
importAdder, err := createImportAdder(context, fixContext, typeChecker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seenClassDeclarations := collections.Set[*ast.Node]{}
|
||||
|
||||
for _, diag := range getAllDiagnostics(context, fixContext.Program, fixContext.SourceFile) {
|
||||
if containsErrorCode(fixClassIncorrectlyImplementsInterfaceErrorCodes, diag.Code()) {
|
||||
classDeclaration := getClass(fixContext.SourceFile, core.NewTextRange(diag.Pos(), diag.End()))
|
||||
if classDeclaration == nil {
|
||||
continue
|
||||
}
|
||||
if seenClassDeclarations.AddIfAbsent(classDeclaration) {
|
||||
implementsTypes := ast.GetImplementsTypeNodes(classDeclaration)
|
||||
for _, implementedTypeNode := range implementsTypes {
|
||||
addChanges(context, fixContext, changeTracker, importAdder, typeChecker, classDeclaration, implementedTypeNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes := getChanges(changeTracker, importAdder, fixContext.SourceFile)
|
||||
if len(changes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &CombinedCodeActions{
|
||||
Description: diagnostics.Implement_all_unimplemented_interfaces.Localize(locale.FromContext(context)),
|
||||
Changes: changes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func addChanges(context context.Context, fixContext *CodeFixContext, changeTracker *change.Tracker, importAdder autoimport.ImportAdder, typeChecker *checker.Checker, classDeclaration *ast.Node, implementedTypeNode *ast.Node) {
|
||||
missingMemberFixer := newMissingMemberFixer(changeTracker, fixContext.Program, typeChecker, fixContext.LS.UserPreferences(), importAdder, locale.FromContext(context))
|
||||
constructor := getConstructor(classDeclaration)
|
||||
implementedType := typeChecker.GetTypeAtLocation(implementedTypeNode)
|
||||
classType := typeChecker.GetTypeAtLocation(classDeclaration)
|
||||
|
||||
if typeChecker.GetNumberIndexType(classType) == nil {
|
||||
member := missingMemberFixer.createIndexSignatureDeclarationFromType(classDeclaration, implementedType, typeChecker.GetNumberType())
|
||||
if member != nil {
|
||||
insertInterfaceMemberNode(changeTracker, fixContext.SourceFile, classDeclaration, constructor, member)
|
||||
}
|
||||
}
|
||||
|
||||
if typeChecker.GetStringIndexType(classType) == nil {
|
||||
member := missingMemberFixer.createIndexSignatureDeclarationFromType(classDeclaration, implementedType, typeChecker.GetStringType())
|
||||
if member != nil {
|
||||
insertInterfaceMemberNode(changeTracker, fixContext.SourceFile, classDeclaration, constructor, member)
|
||||
}
|
||||
}
|
||||
|
||||
missingMembers := getMissingMembers(typeChecker, classDeclaration, []*checker.Type{implementedType})
|
||||
for _, member := range missingMembers {
|
||||
memberNodes := missingMemberFixer.createMemberFromSymbol(member, classDeclaration, fixContext.SourceFile, nil /*body*/, preserveOptionalFlagsAll)
|
||||
for _, memberNode := range memberNodes {
|
||||
insertInterfaceMemberNode(changeTracker, fixContext.SourceFile, classDeclaration, constructor, memberNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getChanges(changeTracker *change.Tracker, importAdder autoimport.ImportAdder, sourceFile *ast.SourceFile) []*lsproto.TextEdit {
|
||||
fileChanges := changeTracker.GetChanges()[sourceFile.FileName()]
|
||||
if importAdder != nil && importAdder.HasFixes() {
|
||||
fileChanges = append(fileChanges, importAdder.Edits()...)
|
||||
}
|
||||
return fileChanges
|
||||
}
|
||||
|
||||
func insertInterfaceMemberNode(changeTracker *change.Tracker, sourceFile *ast.SourceFile, classDeclaration *ast.Node, constructor *ast.Node, member *ast.Node) {
|
||||
if constructor == nil {
|
||||
changeTracker.InsertMemberAtStart(sourceFile, classDeclaration, member)
|
||||
} else {
|
||||
changeTracker.InsertNodeAfter(sourceFile, constructor, member)
|
||||
}
|
||||
}
|
||||
|
||||
func getClass(sourceFile *ast.SourceFile, span core.TextRange) *ast.Node {
|
||||
token := astnav.GetTokenAtPosition(sourceFile, span.Pos())
|
||||
if token == nil {
|
||||
return nil
|
||||
}
|
||||
return ast.GetContainingClass(token)
|
||||
}
|
||||
|
||||
func getConstructor(classDeclaration *ast.Node) *ast.Node {
|
||||
if classDeclaration == nil || classDeclaration.MemberList() == nil {
|
||||
return nil
|
||||
}
|
||||
for _, member := range classDeclaration.MemberList().Nodes {
|
||||
if member != nil && ast.IsConstructorDeclaration(member) {
|
||||
return member
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getMissingMembers(typeChecker *checker.Checker, classDeclaration *ast.Node, implementedTypes []*checker.Type) []*ast.Symbol {
|
||||
inheritedMembers := getInheritedMembers(typeChecker, classDeclaration)
|
||||
seenMembers := make(map[string]*ast.Symbol)
|
||||
|
||||
var classMembers ast.SymbolTable
|
||||
if classDeclaration.Symbol() != nil {
|
||||
classMembers = classDeclaration.Symbol().Members
|
||||
}
|
||||
|
||||
var missingMembers []*ast.Symbol
|
||||
for _, implementedType := range implementedTypes {
|
||||
for _, symbol := range typeChecker.GetPropertiesOfType(implementedType) {
|
||||
if symbol == nil {
|
||||
continue
|
||||
}
|
||||
if classMembers != nil && classMembers[symbol.Name] != nil {
|
||||
continue
|
||||
}
|
||||
if inheritedMembers[symbol.Name] != nil || seenMembers[symbol.Name] != nil {
|
||||
continue
|
||||
}
|
||||
flags := checker.GetDeclarationModifierFlagsFromSymbol(symbol)
|
||||
if flags&ast.ModifierFlagsPrivate == 0 {
|
||||
seenMembers[symbol.Name] = symbol
|
||||
missingMembers = append(missingMembers, symbol)
|
||||
}
|
||||
}
|
||||
}
|
||||
return missingMembers
|
||||
}
|
||||
|
||||
func getInheritedMembers(typeChecker *checker.Checker, classDeclaration *ast.Node) ast.SymbolTable {
|
||||
typeNode := ast.GetClassExtendsHeritageElement(classDeclaration)
|
||||
if typeNode == nil {
|
||||
return ast.SymbolTable{}
|
||||
}
|
||||
|
||||
baseType := typeChecker.GetTypeAtLocation(typeNode.AsNode())
|
||||
if baseType == nil {
|
||||
return ast.SymbolTable{}
|
||||
}
|
||||
|
||||
inheritedMembers := make(ast.SymbolTable)
|
||||
for _, symbol := range typeChecker.GetPropertiesOfType(baseType) {
|
||||
if symbol == nil {
|
||||
continue
|
||||
}
|
||||
flags := checker.GetDeclarationModifierFlagsFromSymbol(symbol)
|
||||
if flags&ast.ModifierFlagsPrivate == 0 {
|
||||
inheritedMembers[symbol.Name] = symbol
|
||||
}
|
||||
}
|
||||
return inheritedMembers
|
||||
}
|
||||
|
||||
func createImportAdder(context context.Context, fixContext *CodeFixContext, typeChecker *checker.Checker) (autoimport.ImportAdder, error) {
|
||||
view, err := fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if view == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return autoimport.NewImportAdder(context, fixContext.Program, typeChecker, fixContext.SourceFile, view, fixContext.LS.FormatOptions(), fixContext.LS.converters, fixContext.LS.UserPreferences()), nil
|
||||
}
|
||||
1419
tools/tsgo/internal/ls/codeactions_fixmissingtypeannotation.go
Normal file
1419
tools/tsgo/internal/ls/codeactions_fixmissingtypeannotation.go
Normal file
File diff suppressed because it is too large
Load Diff
452
tools/tsgo/internal/ls/codeactions_importfixes.go
Normal file
452
tools/tsgo/internal/ls/codeactions_importfixes.go
Normal file
@@ -0,0 +1,452 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/ls/autoimport"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
var importFixErrorCodes = []int32{
|
||||
diagnostics.Cannot_find_name_0.Code(),
|
||||
diagnostics.Cannot_find_name_0_Did_you_mean_1.Code(),
|
||||
diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0.Code(),
|
||||
diagnostics.Cannot_find_name_0_Did_you_mean_the_static_member_1_0.Code(),
|
||||
diagnostics.Cannot_find_namespace_0.Code(),
|
||||
diagnostics.X_0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.Code(),
|
||||
diagnostics.X_0_only_refers_to_a_type_but_is_being_used_as_a_value_here.Code(),
|
||||
diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer.Code(),
|
||||
diagnostics.X_0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type.Code(),
|
||||
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slashjquery.Code(),
|
||||
diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_1_or_later.Code(),
|
||||
diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_include_dom.Code(),
|
||||
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_save_dev_types_Slashjest_or_npm_i_save_dev_types_Slashmocha_and_then_add_jest_or_mocha_to_the_types_field_in_your_tsconfig.Code(),
|
||||
diagnostics.Cannot_find_name_0_Did_you_mean_to_write_this_in_an_async_function.Code(),
|
||||
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_save_dev_types_Slashjquery_and_then_add_jquery_to_the_types_field_in_your_tsconfig.Code(),
|
||||
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_save_dev_types_Slashjest_or_npm_i_save_dev_types_Slashmocha.Code(),
|
||||
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashnode.Code(),
|
||||
diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_save_dev_types_Slashnode_and_then_add_node_to_the_types_field_in_your_tsconfig.Code(),
|
||||
diagnostics.Cannot_find_namespace_0_Did_you_mean_1.Code(),
|
||||
diagnostics.Cannot_extend_an_interface_0_Did_you_mean_implements.Code(),
|
||||
diagnostics.This_JSX_tag_requires_0_to_be_in_scope_but_it_could_not_be_found.Code(),
|
||||
}
|
||||
|
||||
const (
|
||||
importFixID = "fixMissingImport"
|
||||
)
|
||||
|
||||
// ImportFixProvider is the CodeFixProvider for import-related fixes
|
||||
var ImportFixProvider = &CodeFixProvider{
|
||||
ErrorCodes: importFixErrorCodes,
|
||||
GetCodeActions: getImportCodeActions,
|
||||
FixIds: []string{importFixID},
|
||||
GetAllCodeActions: getAllImportCodeActions,
|
||||
}
|
||||
|
||||
type fixInfo struct {
|
||||
fix *autoimport.Fix
|
||||
symbolName string
|
||||
errorIdentifierText string
|
||||
isJsxNamespaceFix bool
|
||||
}
|
||||
|
||||
func getImportCodeActions(ctx context.Context, fixContext *CodeFixContext) ([]*CodeAction, error) {
|
||||
info, err := getFixInfos(ctx, fixContext, fixContext.ErrorCode, fixContext.Span.Pos())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(info) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var actions []*CodeAction
|
||||
for _, fixInfo := range info {
|
||||
edits, description := fixInfo.fix.Edits(
|
||||
ctx,
|
||||
fixContext.SourceFile,
|
||||
fixContext.Program.Options(),
|
||||
fixContext.LS.FormatOptions(),
|
||||
fixContext.LS.converters,
|
||||
fixContext.LS.UserPreferences(),
|
||||
)
|
||||
|
||||
actions = append(actions, &CodeAction{
|
||||
Description: description,
|
||||
Changes: edits,
|
||||
FixID: importFixID,
|
||||
FixAllDescription: diagnostics.Add_all_missing_imports.Localize(locale.FromContext(ctx)),
|
||||
})
|
||||
}
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
func getAllImportCodeActions(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) {
|
||||
if tspath.IsDynamicFileName(fixContext.SourceFile.FileName()) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
allDiagnostics := fixContext.Program.GetSemanticDiagnostics(ctx, fixContext.SourceFile)
|
||||
|
||||
var importDiags []*ast.Diagnostic
|
||||
for _, diag := range allDiagnostics {
|
||||
if containsErrorCode(importFixErrorCodes, diag.Code()) {
|
||||
importDiags = append(importDiags, diag)
|
||||
}
|
||||
}
|
||||
|
||||
if len(importDiags) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
view, err := fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if view == nil {
|
||||
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
|
||||
}
|
||||
|
||||
ch, done := fixContext.Program.GetTypeChecker(ctx)
|
||||
defer done()
|
||||
|
||||
importAdder := autoimport.NewImportAdder(
|
||||
ctx,
|
||||
fixContext.Program,
|
||||
ch,
|
||||
fixContext.SourceFile,
|
||||
view,
|
||||
fixContext.LS.FormatOptions(),
|
||||
fixContext.LS.converters,
|
||||
fixContext.LS.UserPreferences(),
|
||||
)
|
||||
|
||||
for _, diag := range importDiags {
|
||||
if err := addImportFromDiagnostic(ctx, importAdder, diag, fixContext); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !importAdder.HasFixes() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &CombinedCodeActions{
|
||||
Description: diagnostics.Add_all_missing_imports.Localize(locale.FromContext(ctx)),
|
||||
Changes: importAdder.Edits(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// addImportFromDiagnostic finds the best import fix for a diagnostic and adds it to the adder.
|
||||
func addImportFromDiagnostic(ctx context.Context, importAdder autoimport.ImportAdder, diag *ast.Diagnostic, fixContext *CodeFixContext) error {
|
||||
diagFixContext := &CodeFixContext{
|
||||
SourceFile: fixContext.SourceFile,
|
||||
Span: core.NewTextRange(diag.Pos(), diag.End()),
|
||||
ErrorCode: diag.Code(),
|
||||
Program: fixContext.Program,
|
||||
LS: fixContext.LS,
|
||||
}
|
||||
|
||||
infos, err := getFixInfos(ctx, diagFixContext, diag.Code(), diag.Pos())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(infos) > 0 {
|
||||
importAdder.AddImportFix(infos[0].fix)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFixInfos(ctx context.Context, fixContext *CodeFixContext, errorCode int32, pos int) ([]*fixInfo, error) {
|
||||
// Can't compute import fixes for dynamic/untitled files since they don't have real file paths
|
||||
if tspath.IsDynamicFileName(fixContext.SourceFile.FileName()) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
symbolToken := astnav.GetTokenAtPosition(fixContext.SourceFile, pos)
|
||||
|
||||
var view *autoimport.View
|
||||
var info []*fixInfo
|
||||
|
||||
if errorCode == diagnostics.X_0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.Code() {
|
||||
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
|
||||
info = getFixesInfoForUMDImport(ctx, fixContext, symbolToken, view)
|
||||
} else if !ast.IsIdentifier(symbolToken) {
|
||||
return nil, nil
|
||||
} else if errorCode == diagnostics.X_0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type.Code() {
|
||||
ch, done := fixContext.Program.GetTypeChecker(ctx)
|
||||
defer done()
|
||||
compilerOptions := fixContext.Program.Options()
|
||||
symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
|
||||
|
||||
var allTypeOnlyFixes []*fixInfo
|
||||
for _, sn := range symbolNames {
|
||||
if !sn.isTypeOnly {
|
||||
continue
|
||||
}
|
||||
fix := getTypeOnlyPromotionFix(ctx, fixContext.SourceFile, symbolToken, sn.name, fixContext.Program)
|
||||
if fix != nil {
|
||||
allTypeOnlyFixes = append(allTypeOnlyFixes, &fixInfo{fix: fix, symbolName: sn.name, errorIdentifierText: symbolToken.Text()})
|
||||
}
|
||||
}
|
||||
|
||||
// For JSX opening tags, there can be separate type-only errors for both the tag name
|
||||
// identifier and the JSX namespace identifier. When both produce valid fixes, we
|
||||
// disambiguate using the diagnostic message, which quotes the symbol name in single
|
||||
// quotes (e.g., "'React' cannot be used as a value..."). If filtering yields nothing
|
||||
// (e.g., due to localization), fall back to returning all candidates.
|
||||
diagnosticMessage := ""
|
||||
if fixContext.Diagnostic != nil {
|
||||
diagnosticMessage = fixContext.Diagnostic.Message.AsString()
|
||||
}
|
||||
if len(allTypeOnlyFixes) > 1 && diagnosticMessage != "" {
|
||||
for _, fi := range allTypeOnlyFixes {
|
||||
if strings.Contains(diagnosticMessage, "'"+fi.symbolName+"'") {
|
||||
info = append(info, fi)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(info) == 0 {
|
||||
info = allTypeOnlyFixes
|
||||
}
|
||||
return info, nil
|
||||
} else {
|
||||
var err error
|
||||
view, err = fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if view != nil {
|
||||
info = getFixesInfoForNonUMDImport(ctx, fixContext, symbolToken, view)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort fixes by preference
|
||||
if view == nil {
|
||||
view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile)
|
||||
}
|
||||
return sortFixInfo(info, fixContext, view), nil
|
||||
}
|
||||
|
||||
func getFixesInfoForUMDImport(ctx context.Context, fixContext *CodeFixContext, token *ast.Node, view *autoimport.View) []*fixInfo {
|
||||
ch, done := fixContext.Program.GetTypeChecker(ctx)
|
||||
defer done()
|
||||
|
||||
umdSymbol := getUmdSymbol(token, ch)
|
||||
if umdSymbol == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
export := autoimport.SymbolToExport(umdSymbol, ch)
|
||||
isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(token)
|
||||
|
||||
var result []*fixInfo
|
||||
for _, fix := range view.GetFixes(ctx, export, false, isValidTypeOnlyUseSite, nil) {
|
||||
errorIdentifierText := ""
|
||||
if ast.IsIdentifier(token) {
|
||||
errorIdentifierText = token.Text()
|
||||
}
|
||||
result = append(result, &fixInfo{
|
||||
fix: fix,
|
||||
symbolName: umdSymbol.Name,
|
||||
errorIdentifierText: errorIdentifierText,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getUmdSymbol(token *ast.Node, ch *checker.Checker) *ast.Symbol {
|
||||
// try the identifier to see if it is the umd symbol
|
||||
var umdSymbol *ast.Symbol
|
||||
if ast.IsIdentifier(token) {
|
||||
umdSymbol = ch.GetResolvedSymbol(token)
|
||||
}
|
||||
if isUMDExportSymbol(umdSymbol) {
|
||||
return umdSymbol
|
||||
}
|
||||
|
||||
// The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`.
|
||||
parent := token.Parent
|
||||
if (ast.IsJsxOpeningLikeElement(parent) && parent.TagName() == token) ||
|
||||
ast.IsJsxOpeningFragment(parent) {
|
||||
var location *ast.Node
|
||||
if ast.IsJsxOpeningLikeElement(parent) {
|
||||
location = token
|
||||
} else {
|
||||
location = parent
|
||||
}
|
||||
jsxNamespace := ch.GetJsxNamespace(parent)
|
||||
parentSymbol := ch.ResolveName(jsxNamespace, location, ast.SymbolFlagsValue, false /* excludeGlobals */)
|
||||
if isUMDExportSymbol(parentSymbol) {
|
||||
return parentSymbol
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isUMDExportSymbol(symbol *ast.Symbol) bool {
|
||||
return symbol != nil && len(symbol.Declarations) > 0 &&
|
||||
symbol.Declarations[0] != nil &&
|
||||
ast.IsNamespaceExportDeclaration(symbol.Declarations[0])
|
||||
}
|
||||
|
||||
func getFixesInfoForNonUMDImport(ctx context.Context, fixContext *CodeFixContext, symbolToken *ast.Node, view *autoimport.View) []*fixInfo {
|
||||
ch, done := fixContext.Program.GetTypeChecker(ctx)
|
||||
defer done()
|
||||
compilerOptions := fixContext.Program.Options()
|
||||
|
||||
isValidTypeOnlyUseSite := ast.IsValidTypeOnlyAliasUseSite(symbolToken)
|
||||
symbolNames := getSymbolNamesToImport(fixContext.SourceFile, ch, symbolToken, compilerOptions)
|
||||
var allInfo []*fixInfo
|
||||
|
||||
// Compute usage position for JSDoc import type fixes
|
||||
usagePosition := fixContext.LS.converters.PositionToLineAndCharacter(fixContext.SourceFile, core.TextPos(scanner.GetTokenPosOfNode(symbolToken, fixContext.SourceFile, false)))
|
||||
|
||||
for _, sn := range symbolNames {
|
||||
// Type-only imports are handled by the promotion code path, not the auto-import path.
|
||||
if sn.isTypeOnly {
|
||||
continue
|
||||
}
|
||||
|
||||
symbolName := sn.name
|
||||
// "default" is a keyword and not a legal identifier for the import
|
||||
if symbolName == "default" {
|
||||
continue
|
||||
}
|
||||
|
||||
isJSXTagName := symbolName == symbolToken.Text() && ast.IsJsxTagName(symbolToken)
|
||||
queryKind := autoimport.QueryKindExactMatch
|
||||
if isJSXTagName {
|
||||
queryKind = autoimport.QueryKindCaseInsensitiveMatch
|
||||
}
|
||||
|
||||
exports := view.Search(symbolName, queryKind)
|
||||
for _, export := range exports {
|
||||
if isJSXTagName && !(export.Name() == symbolName || export.IsRenameable()) {
|
||||
continue
|
||||
}
|
||||
|
||||
fixes := view.GetFixes(ctx, export, isJSXTagName, isValidTypeOnlyUseSite, &usagePosition)
|
||||
for _, fix := range fixes {
|
||||
allInfo = append(allInfo, &fixInfo{
|
||||
fix: fix,
|
||||
symbolName: symbolName,
|
||||
isJsxNamespaceFix: symbolName != symbolToken.Text(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allInfo
|
||||
}
|
||||
|
||||
func getTypeOnlyPromotionFix(ctx context.Context, sourceFile *ast.SourceFile, symbolToken *ast.Node, symbolName string, program *compiler.Program) *autoimport.Fix {
|
||||
ch, done := program.GetTypeChecker(ctx)
|
||||
defer done()
|
||||
|
||||
// Get the symbol at the token location
|
||||
symbol := ch.ResolveName(symbolName, symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */)
|
||||
if symbol == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the type-only alias declaration
|
||||
typeOnlyAliasDeclaration := ch.GetTypeOnlyAliasDeclaration(symbol)
|
||||
if typeOnlyAliasDeclaration == nil || ast.GetSourceFileOfNode(typeOnlyAliasDeclaration) != sourceFile {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &autoimport.Fix{
|
||||
AutoImportFix: &lsproto.AutoImportFix{
|
||||
Kind: lsproto.AutoImportFixKindPromoteTypeOnly,
|
||||
},
|
||||
TypeOnlyAliasDeclaration: typeOnlyAliasDeclaration,
|
||||
}
|
||||
}
|
||||
|
||||
type symbolNameInfo struct {
|
||||
name string
|
||||
isTypeOnly bool // whether the symbol currently resolves to a type-only import
|
||||
}
|
||||
|
||||
func getSymbolNamesToImport(sourceFile *ast.SourceFile, ch *checker.Checker, symbolToken *ast.Node, compilerOptions *core.CompilerOptions) []symbolNameInfo {
|
||||
parent := symbolToken.Parent
|
||||
if (ast.IsJsxOpeningLikeElement(parent) || ast.IsJsxClosingElement(parent)) &&
|
||||
parent.TagName() == symbolToken &&
|
||||
jsxModeNeedsExplicitImport(compilerOptions.Jsx) {
|
||||
jsxNamespace := ch.GetJsxNamespace(sourceFile.AsNode())
|
||||
if needsJsxNamespaceFix(jsxNamespace, symbolToken, ch) {
|
||||
var result []symbolNameInfo
|
||||
if !scanner.IsIntrinsicJsxName(symbolToken.Text()) {
|
||||
compSymbol := ch.ResolveName(symbolToken.Text(), symbolToken, ast.SymbolFlagsValue, false /* excludeGlobals */)
|
||||
if compSymbol == nil {
|
||||
result = append(result, symbolNameInfo{name: symbolToken.Text()})
|
||||
} else if ch.GetTypeOnlyAliasDeclaration(compSymbol) != nil {
|
||||
result = append(result, symbolNameInfo{name: symbolToken.Text(), isTypeOnly: true})
|
||||
}
|
||||
}
|
||||
nsIsTypeOnly := false
|
||||
if nsSymbol := ch.ResolveName(jsxNamespace, symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */); nsSymbol != nil {
|
||||
nsIsTypeOnly = ch.GetTypeOnlyAliasDeclaration(nsSymbol) != nil
|
||||
}
|
||||
result = append(result, symbolNameInfo{name: jsxNamespace, isTypeOnly: nsIsTypeOnly})
|
||||
return result
|
||||
}
|
||||
}
|
||||
tokenIsTypeOnly := false
|
||||
if sym := ch.ResolveName(symbolToken.Text(), symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */); sym != nil {
|
||||
tokenIsTypeOnly = ch.GetTypeOnlyAliasDeclaration(sym) != nil
|
||||
}
|
||||
return []symbolNameInfo{{name: symbolToken.Text(), isTypeOnly: tokenIsTypeOnly}}
|
||||
}
|
||||
|
||||
func needsJsxNamespaceFix(jsxNamespace string, symbolToken *ast.Node, ch *checker.Checker) bool {
|
||||
if scanner.IsIntrinsicJsxName(symbolToken.Text()) {
|
||||
return true
|
||||
}
|
||||
namespaceSymbol := ch.ResolveName(jsxNamespace, symbolToken, ast.SymbolFlagsValue, true /* excludeGlobals */)
|
||||
if namespaceSymbol == nil {
|
||||
return true
|
||||
}
|
||||
if slices.ContainsFunc(namespaceSymbol.Declarations, ast.IsTypeOnlyImportOrExportDeclaration) {
|
||||
return (namespaceSymbol.Flags & ast.SymbolFlagsValue) == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func jsxModeNeedsExplicitImport(jsx core.JsxEmit) bool {
|
||||
return jsx == core.JsxEmitReact || jsx == core.JsxEmitReactNative
|
||||
}
|
||||
|
||||
func sortFixInfo(fixes []*fixInfo, fixContext *CodeFixContext, view *autoimport.View) []*fixInfo {
|
||||
if len(fixes) == 0 {
|
||||
return fixes
|
||||
}
|
||||
|
||||
// Create a copy to avoid modifying the original
|
||||
sorted := make([]*fixInfo, len(fixes))
|
||||
copy(sorted, fixes)
|
||||
|
||||
// Sort by:
|
||||
// 1. JSX namespace fixes last
|
||||
// 2. Fix comparison using view.CompareFixes
|
||||
slices.SortFunc(sorted, func(a, b *fixInfo) int {
|
||||
// JSX namespace fixes should come last
|
||||
if cmp := core.CompareBooleans(a.isJsxNamespaceFix, b.isJsxNamespaceFix); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return view.CompareFixesForSorting(a.fix, b.fix)
|
||||
})
|
||||
|
||||
return sorted
|
||||
}
|
||||
498
tools/tsgo/internal/ls/codeactions_missingmemberfixer.go
Normal file
498
tools/tsgo/internal/ls/codeactions_missingmemberfixer.go
Normal file
@@ -0,0 +1,498 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/ls/autoimport"
|
||||
"github.com/microsoft/typescript-go/internal/ls/change"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/nodebuilder"
|
||||
)
|
||||
|
||||
type preserveOptionalFlags int
|
||||
|
||||
const (
|
||||
preserveOptionalFlagsMethod preserveOptionalFlags = 1 << iota
|
||||
preserveOptionalFlagsProperty
|
||||
preserveOptionalFlagsAll = preserveOptionalFlagsMethod | preserveOptionalFlagsProperty
|
||||
)
|
||||
|
||||
type missingMemberFixer struct {
|
||||
changeTracker *change.Tracker
|
||||
typeChecker *checker.Checker
|
||||
program *compiler.Program
|
||||
preferences lsutil.UserPreferences
|
||||
importAdder autoimport.ImportAdder
|
||||
locale locale.Locale
|
||||
}
|
||||
|
||||
func newMissingMemberFixer(changeTracker *change.Tracker, program *compiler.Program, typeChecker *checker.Checker, preferences lsutil.UserPreferences, importAdder autoimport.ImportAdder, locale locale.Locale) *missingMemberFixer {
|
||||
return &missingMemberFixer{
|
||||
changeTracker: changeTracker,
|
||||
typeChecker: typeChecker,
|
||||
program: program,
|
||||
preferences: preferences,
|
||||
importAdder: importAdder,
|
||||
locale: locale,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) createNodeBuilder() (*checker.NodeBuilder, map[*ast.IdentifierNode]*ast.Symbol) {
|
||||
idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol)
|
||||
nodeBuilder := checker.NewNodeBuilderEx(f.typeChecker, f.changeTracker.EmitContext, idToSymbol)
|
||||
return nodeBuilder, idToSymbol
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) createMemberFromSymbol(symbol *ast.Symbol, enclosingDeclaration *ast.Node, sourceFile *ast.SourceFile, body *ast.FunctionBody, preserveOptional preserveOptionalFlags) []*ast.Node {
|
||||
declarations := symbol.Declarations
|
||||
declaration := core.FirstOrNil(declarations)
|
||||
|
||||
quotePreference := lsutil.GetQuotePreference(sourceFile, f.preferences)
|
||||
ambient := enclosingDeclaration.Flags&ast.NodeFlagsAmbient != 0
|
||||
optional := symbol.Flags&ast.SymbolFlagsOptional != 0
|
||||
kind := ast.KindPropertySignature
|
||||
if declaration != nil {
|
||||
kind = declaration.Kind
|
||||
}
|
||||
declarationName := createDeclarationName(f.changeTracker.NodeFactory, f.typeChecker, symbol, declaration)
|
||||
modifiers := f.createModifiers(symbol, declaration)
|
||||
|
||||
flags := nodebuilder.FlagsNoTruncation
|
||||
if quotePreference == lsutil.QuotePreferenceSingle {
|
||||
flags |= nodebuilder.FlagsUseSingleQuotesForStringLiteralType
|
||||
}
|
||||
|
||||
t := f.typeChecker.GetWidenedType(f.typeChecker.GetTypeOfSymbolAtLocation(symbol, enclosingDeclaration))
|
||||
var nodes []*ast.Node
|
||||
|
||||
switch kind {
|
||||
case ast.KindPropertySignature, ast.KindPropertyDeclaration:
|
||||
nodeBuilder, idToSymbol := f.createNodeBuilder()
|
||||
typeNode := f.createTypeNode(t, enclosingDeclaration, flags, nodeBuilder, idToSymbol)
|
||||
var questionToken *ast.TokenNode
|
||||
if optional && preserveOptional&preserveOptionalFlagsProperty != 0 {
|
||||
questionToken = f.changeTracker.NodeFactory.NewToken(ast.KindQuestionToken)
|
||||
}
|
||||
return append(nodes, f.changeTracker.NodeFactory.NewPropertyDeclaration(modifiers, createPropertyName(f.changeTracker.NodeFactory, declarationName, quotePreference), questionToken, typeNode, nil /*initializer*/))
|
||||
|
||||
case ast.KindGetAccessor, ast.KindSetAccessor:
|
||||
nodeBuilder, idToSymbol := f.createNodeBuilder()
|
||||
accessors := ast.GetAllAccessorDeclarations(symbol.Declarations, declaration)
|
||||
var orderedAccessors []*ast.Node
|
||||
if accessors.SecondAccessor == nil {
|
||||
orderedAccessors = append(orderedAccessors, accessors.FirstAccessor)
|
||||
} else {
|
||||
orderedAccessors = append(orderedAccessors, accessors.FirstAccessor, accessors.SecondAccessor)
|
||||
}
|
||||
|
||||
for _, accessor := range orderedAccessors {
|
||||
if ast.IsGetAccessorDeclaration(accessor) {
|
||||
nodes = append(
|
||||
nodes,
|
||||
f.changeTracker.NodeFactory.NewGetAccessorDeclaration(
|
||||
modifiers, createPropertyName(f.changeTracker.NodeFactory, declarationName, quotePreference),
|
||||
nil /*typeParameters*/, nil /*parameters*/, f.createTypeNode(t, enclosingDeclaration, flags, nodeBuilder, idToSymbol), nil /*fullSignature*/, f.createBody(body, ambient, quotePreference),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if ast.IsSetAccessorDeclaration(accessor) {
|
||||
parameter := checker.GetSetAccessorValueParameter(accessor)
|
||||
if parameter == nil {
|
||||
panic("Expected set accessor to have a parameter.")
|
||||
}
|
||||
|
||||
nodes = append(
|
||||
nodes, f.changeTracker.NodeFactory.NewSetAccessorDeclaration(
|
||||
modifiers, createPropertyName(f.changeTracker.NodeFactory, declarationName, quotePreference),
|
||||
nil /*typeParameters*/, createDummyParameters(f.changeTracker.NodeFactory, 1, []string{parameter.Name().Text()}, []*ast.TypeNode{f.createTypeNode(t, enclosingDeclaration, flags, nodeBuilder, idToSymbol)}, 1, ast.IsInJSFile(enclosingDeclaration)),
|
||||
nil /*type*/, nil /*fullSignature*/, f.createBody(body, ambient, quotePreference),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
|
||||
case ast.KindMethodSignature, ast.KindMethodDeclaration:
|
||||
signatures := f.getCallSignatures(t)
|
||||
preserveOptional := optional && preserveOptional&preserveOptionalFlagsMethod != 0
|
||||
if len(signatures) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(declarations) == 1 {
|
||||
method := f.createSignatureDeclarationFromSignature(core.FirstOrNil(signatures), ast.KindMethodDeclaration, sourceFile, enclosingDeclaration, f.createBody(body, ambient, quotePreference), modifiers, declarationName, preserveOptional)
|
||||
if method != nil {
|
||||
nodes = append(nodes, method)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
for _, signature := range signatures {
|
||||
if signature.Declaration() != nil && signature.Declaration().Flags&ast.NodeFlagsAmbient != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
method := f.createSignatureDeclarationFromSignature(signature, ast.KindMethodDeclaration, sourceFile, enclosingDeclaration, nil, modifiers, declarationName, preserveOptional)
|
||||
if method != nil {
|
||||
nodes = append(nodes, method)
|
||||
}
|
||||
}
|
||||
|
||||
if ambient {
|
||||
return nodes
|
||||
}
|
||||
|
||||
if len(declarations) > len(signatures) {
|
||||
signature := f.typeChecker.GetSignatureFromDeclaration(core.LastOrNil(declarations))
|
||||
method := f.createSignatureDeclarationFromSignature(signature, ast.KindMethodDeclaration, sourceFile, enclosingDeclaration, f.createBody(body, ambient, quotePreference), modifiers, declarationName, preserveOptional)
|
||||
if method != nil {
|
||||
nodes = append(nodes, method)
|
||||
}
|
||||
} else {
|
||||
method := f.createSignatureDeclarationFromSignatures(signatures, declarationName, preserveOptional, modifiers, quotePreference, body, enclosingDeclaration)
|
||||
if method != nil {
|
||||
nodes = append(nodes, method)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) getCallSignatures(t *checker.Type) []*checker.Signature {
|
||||
if t.IsUnion() {
|
||||
return core.FlatMap(t.Types(), f.typeChecker.GetCallSignatures)
|
||||
}
|
||||
return f.typeChecker.GetCallSignatures(t)
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) createTypeNode(t *checker.Type, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, nodeBuilder *checker.NodeBuilder, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) *ast.TypeNode {
|
||||
return f.importTypeNode(nodeBuilder.TypeToTypeNode(t, enclosingDeclaration, flags, nodebuilder.InternalFlagsNone, nil /*tracker*/), idToSymbol)
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) createModifiers(symbol *ast.Symbol, declaration *ast.Node) *ast.ModifierList {
|
||||
modifierFlags := ast.ModifierFlagsNone
|
||||
if declaration != nil {
|
||||
effective := checker.GetDeclarationModifierFlagsFromSymbol(symbol)
|
||||
modifierFlags = effective & ast.ModifierFlagsStatic
|
||||
if effective&ast.ModifierFlagsPublic != 0 {
|
||||
modifierFlags |= ast.ModifierFlagsPublic
|
||||
} else if effective&ast.ModifierFlagsProtected != 0 {
|
||||
modifierFlags |= ast.ModifierFlagsProtected
|
||||
}
|
||||
if ast.IsAutoAccessorPropertyDeclaration(declaration) {
|
||||
modifierFlags |= ast.ModifierFlagsAccessor
|
||||
}
|
||||
}
|
||||
if f.shouldAddOverrideKeyword(declaration) {
|
||||
modifierFlags |= ast.ModifierFlagsOverride
|
||||
}
|
||||
if modifierFlags == ast.ModifierFlagsNone {
|
||||
return nil
|
||||
}
|
||||
return f.changeTracker.NodeFactory.NewModifierList(ast.CreateModifiersFromModifierFlags(modifierFlags, f.changeTracker.NodeFactory.NewModifier))
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) shouldAddOverrideKeyword(declaration *ast.Node) bool {
|
||||
return declaration != nil && f.program.Options().NoImplicitOverride.IsTrue() && ast.HasAbstractModifier(declaration)
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) createSignatureDeclarationFromSignature(signature *checker.Signature, kind ast.Kind, sourceFile *ast.SourceFile, enclosingDeclaration *ast.Node, body *ast.FunctionBody, modifiers *ast.ModifierList, name *ast.PropertyName, optional bool) *ast.Node {
|
||||
quotePreference := lsutil.GetQuotePreference(sourceFile, f.preferences)
|
||||
flags := nodebuilder.FlagsNoTruncation | nodebuilder.FlagsSuppressAnyReturnType | nodebuilder.FlagsAllowEmptyTuple
|
||||
if quotePreference == lsutil.QuotePreferenceSingle {
|
||||
flags |= nodebuilder.FlagsUseSingleQuotesForStringLiteralType
|
||||
}
|
||||
|
||||
nodeBuilder, idToSymbol := f.createNodeBuilder()
|
||||
signatureDeclaration := nodeBuilder.SignatureToSignatureDeclaration(signature, kind, enclosingDeclaration, flags, nodebuilder.InternalFlagsAllowUnresolvedNames, nil /*tracker*/)
|
||||
if signatureDeclaration == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
isJS := ast.IsInJSFile(enclosingDeclaration)
|
||||
parameters := signatureDeclaration.ParameterList()
|
||||
typeParameters := core.IfElse(isJS, nil, signatureDeclaration.TypeParameterList())
|
||||
typeNode := core.IfElse(isJS, nil, signatureDeclaration.Type())
|
||||
|
||||
if typeParameters != nil && len(typeParameters.Nodes) > 0 {
|
||||
nodes := make([]*ast.Node, 0, len(typeParameters.Nodes))
|
||||
for _, tp := range typeParameters.Nodes {
|
||||
if tp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if ast.IsTypeParameterDeclaration(tp) {
|
||||
typeParameter := tp.AsTypeParameterDeclaration()
|
||||
|
||||
constraint := typeParameter.Constraint
|
||||
if constraint != nil {
|
||||
constraint = f.importTypeNode(constraint, idToSymbol)
|
||||
}
|
||||
|
||||
defaultType := typeParameter.DefaultType
|
||||
if defaultType != nil {
|
||||
defaultType = f.importTypeNode(defaultType, idToSymbol)
|
||||
}
|
||||
|
||||
nodes = append(nodes,
|
||||
f.changeTracker.NodeFactory.UpdateTypeParameterDeclaration(typeParameter, typeParameter.Modifiers(), typeParameter.Name(), constraint, typeParameter.Expression, defaultType))
|
||||
} else {
|
||||
nodes = append(nodes, tp)
|
||||
}
|
||||
}
|
||||
typeParameters = f.changeTracker.NodeFactory.NewNodeList(nodes)
|
||||
}
|
||||
|
||||
if parameters != nil {
|
||||
nodes := make([]*ast.Node, 0, len(parameters.Nodes))
|
||||
for _, p := range parameters.Nodes {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
parameter := p.AsParameterDeclaration()
|
||||
parameterTypeNode := parameter.Type
|
||||
if parameterTypeNode != nil {
|
||||
parameterTypeNode = f.importTypeNode(parameterTypeNode, idToSymbol)
|
||||
}
|
||||
|
||||
nodes = append(nodes,
|
||||
f.changeTracker.NodeFactory.UpdateParameterDeclaration(parameter, parameter.Modifiers(), parameter.DotDotDotToken, parameter.Name(), core.IfElse(isJS, nil, parameter.QuestionToken), parameterTypeNode, parameter.Initializer))
|
||||
}
|
||||
parameters = f.changeTracker.NodeFactory.NewNodeList(nodes)
|
||||
}
|
||||
|
||||
if typeNode != nil {
|
||||
typeNode = f.importTypeNode(typeNode, idToSymbol)
|
||||
}
|
||||
|
||||
var questionToken *ast.TokenNode
|
||||
if optional {
|
||||
questionToken = f.changeTracker.NodeFactory.NewToken(ast.KindQuestionToken)
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case ast.KindFunctionExpression:
|
||||
fn := signatureDeclaration.AsFunctionExpression()
|
||||
return f.changeTracker.NodeFactory.UpdateFunctionExpression(fn, modifiers, fn.AsteriskToken, core.IfElse(name != nil && ast.IsIdentifier(name), name, nil), typeParameters, parameters, typeNode, fn.FullSignature, core.OrElse(body, fn.Body))
|
||||
|
||||
case ast.KindArrowFunction:
|
||||
fn := signatureDeclaration.AsArrowFunction()
|
||||
return f.changeTracker.NodeFactory.UpdateArrowFunction(fn, modifiers, typeParameters, parameters, typeNode, fn.FullSignature, fn.EqualsGreaterThanToken, core.OrElse(body, fn.Body))
|
||||
|
||||
case ast.KindMethodDeclaration:
|
||||
method := signatureDeclaration.AsMethodDeclaration()
|
||||
methodName := core.IfElse(name == nil, f.changeTracker.NodeFactory.NewIdentifier(""), createPropertyName(f.changeTracker.NodeFactory, name, quotePreference))
|
||||
return f.changeTracker.NodeFactory.UpdateMethodDeclaration(method, modifiers, method.AsteriskToken, methodName, questionToken, typeParameters, parameters, typeNode, method.FullSignature, body)
|
||||
|
||||
case ast.KindFunctionDeclaration:
|
||||
fn := signatureDeclaration.AsFunctionDeclaration()
|
||||
return f.changeTracker.NodeFactory.UpdateFunctionDeclaration(fn, modifiers, fn.AsteriskToken, core.IfElse(name != nil && ast.IsIdentifier(name), name, nil), typeParameters, parameters, typeNode, fn.FullSignature, core.OrElse(body, fn.Body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) createSignatureDeclarationFromSignatures(signatures []*checker.Signature, name *ast.PropertyName, optional bool, modifiers *ast.ModifierList, quotePreference lsutil.QuotePreference, body *ast.FunctionBody, enclosingDeclaration *ast.Node) *ast.Node {
|
||||
if len(signatures) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
nodeBuilder, idToSymbol := f.createNodeBuilder()
|
||||
maxArgsSignature := signatures[0]
|
||||
minArgumentCount := signatures[0].MinArgumentCount()
|
||||
|
||||
hasRestParameter := false
|
||||
for _, signature := range signatures {
|
||||
minArgumentCount = min(minArgumentCount, signature.MinArgumentCount())
|
||||
if signature.HasRestParameter() {
|
||||
hasRestParameter = true
|
||||
}
|
||||
if len(signature.Parameters()) >= len(maxArgsSignature.Parameters()) && (!signature.HasRestParameter() || maxArgsSignature.HasRestParameter()) {
|
||||
maxArgsSignature = signature
|
||||
}
|
||||
}
|
||||
|
||||
maxNonRestArgs := len(maxArgsSignature.Parameters()) - core.IfElse(maxArgsSignature.HasRestParameter(), 1, 0)
|
||||
parameterNames := make([]string, 0, len(maxArgsSignature.Parameters()))
|
||||
for _, symbol := range maxArgsSignature.Parameters() {
|
||||
parameterNames = append(parameterNames, symbol.Name)
|
||||
}
|
||||
parameters := createDummyParameters(f.changeTracker.NodeFactory, maxNonRestArgs, parameterNames, nil /*types*/, minArgumentCount, ast.IsInJSFile(enclosingDeclaration))
|
||||
|
||||
if hasRestParameter {
|
||||
restParameterName := "rest"
|
||||
if maxNonRestArgs < len(parameterNames) && parameterNames[maxNonRestArgs] != "" {
|
||||
restParameterName = parameterNames[maxNonRestArgs]
|
||||
}
|
||||
|
||||
var questionToken *ast.QuestionToken
|
||||
if maxNonRestArgs >= minArgumentCount {
|
||||
questionToken = f.changeTracker.NodeFactory.NewToken(ast.KindQuestionToken)
|
||||
}
|
||||
|
||||
parameters.Nodes = append(parameters.Nodes, f.changeTracker.NodeFactory.NewParameterDeclaration(
|
||||
nil /*modifiers*/, f.changeTracker.NodeFactory.NewToken(ast.KindDotDotDotToken),
|
||||
f.changeTracker.NodeFactory.NewIdentifier(restParameterName), questionToken,
|
||||
f.changeTracker.NodeFactory.NewArrayTypeNode(f.changeTracker.NodeFactory.NewKeywordTypeNode(ast.KindUnknownKeyword)), nil, /*initializer*/
|
||||
))
|
||||
}
|
||||
|
||||
methodName := core.IfElse(name == nil, f.changeTracker.NodeFactory.NewIdentifier(""), createPropertyName(f.changeTracker.NodeFactory, name, quotePreference))
|
||||
|
||||
return f.changeTracker.NodeFactory.NewMethodDeclaration(
|
||||
modifiers, nil /*asteriskToken*/, methodName, core.IfElse(optional, f.changeTracker.NodeFactory.NewToken(ast.KindQuestionToken), nil),
|
||||
nil /*typeParameters*/, parameters, f.getReturnTypeFromSignatures(signatures, enclosingDeclaration, nodeBuilder, idToSymbol),
|
||||
nil /*fullSignature*/, f.createBody(body, false /*ambient*/, quotePreference),
|
||||
)
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) getReturnTypeFromSignatures(signatures []*checker.Signature, enclosingDeclaration *ast.Node, nodeBuilder *checker.NodeBuilder, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) *ast.TypeNode {
|
||||
if len(signatures) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
returnTypes := make([]*checker.Type, 0, len(signatures))
|
||||
for _, signature := range signatures {
|
||||
returnTypes = append(returnTypes, f.typeChecker.GetReturnTypeOfSignature(signature))
|
||||
}
|
||||
|
||||
unionType := f.typeChecker.GetUnionType(returnTypes)
|
||||
return f.importTypeNode(nodeBuilder.TypeToTypeNode(unionType, enclosingDeclaration, nodebuilder.FlagsNoTruncation, nodebuilder.InternalFlagsAllowUnresolvedNames, nil /*typeArguments*/), idToSymbol)
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) importTypeNode(typeNode *ast.TypeNode, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) *ast.TypeNode {
|
||||
if typeNode == nil || f.importAdder == nil {
|
||||
return typeNode
|
||||
}
|
||||
|
||||
importedTypeNode, symbols := autoimport.TryGetAutoImportableReferenceFromTypeNode(typeNode, idToSymbol)
|
||||
if importedTypeNode != nil {
|
||||
for _, symbol := range symbols {
|
||||
f.importAdder.AddImportFromExportedSymbol(symbol, true /*isValidTypeOnlyUseSite*/)
|
||||
}
|
||||
return importedTypeNode
|
||||
}
|
||||
|
||||
seen := make(map[*ast.Symbol]bool)
|
||||
for _, symbol := range idToSymbol {
|
||||
if symbol == nil || seen[symbol] {
|
||||
continue
|
||||
}
|
||||
seen[symbol] = true
|
||||
f.importAdder.AddImportFromExportedSymbol(symbol, true /*isValidTypeOnlyUseSite*/)
|
||||
}
|
||||
return typeNode
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) createIndexSignatureDeclarationFromType(classDeclaration *ast.Node, implementedType *checker.Type, keyType *checker.Type) *ast.Node {
|
||||
indexInfo := f.typeChecker.GetIndexInfoOfType(implementedType, keyType)
|
||||
if indexInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
builder := checker.NewNodeBuilder(f.typeChecker, f.changeTracker.EmitContext)
|
||||
return builder.IndexInfoToIndexSignatureDeclaration(indexInfo, classDeclaration, nodebuilder.FlagsNone, nodebuilder.InternalFlagsNone, nil)
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) createBody(body *ast.FunctionBody, ambient bool, quotePreference lsutil.QuotePreference) *ast.FunctionBody {
|
||||
if ambient {
|
||||
return nil
|
||||
}
|
||||
body = f.changeTracker.NodeFactory.DeepCloneNode(body)
|
||||
if body == nil {
|
||||
return f.createStubbedMethodBody(quotePreference)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func (f *missingMemberFixer) createStubbedMethodBody(quotePreference lsutil.QuotePreference) *ast.FunctionBody {
|
||||
tokenFlags := ast.TokenFlagsNone
|
||||
if quotePreference == lsutil.QuotePreferenceSingle {
|
||||
tokenFlags = ast.TokenFlagsSingleQuote
|
||||
}
|
||||
|
||||
return f.changeTracker.NodeFactory.NewBlock(f.changeTracker.NodeFactory.NewNodeList([]*ast.Node{
|
||||
f.changeTracker.NodeFactory.NewThrowStatement(
|
||||
f.changeTracker.NodeFactory.NewNewExpression(
|
||||
f.changeTracker.NodeFactory.NewIdentifier("Error"), nil /*typeArguments*/, f.changeTracker.NodeFactory.NewNodeList([]*ast.Node{
|
||||
f.changeTracker.NodeFactory.NewStringLiteral(diagnostics.Method_not_implemented.Localize(f.locale), tokenFlags),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}), true /*multiLine*/)
|
||||
}
|
||||
|
||||
func createDummyParameters(factory *ast.NodeFactory, argCount int, names []string, types []*ast.TypeNode, minArgumentCount int, inJS bool) *ast.ParameterList {
|
||||
parameters := make([]*ast.Node, 0, argCount)
|
||||
parameterNameCounts := make(map[string]int)
|
||||
|
||||
for i := range argCount {
|
||||
parameterName := ""
|
||||
if i < len(names) && names[i] != "" {
|
||||
parameterName = names[i]
|
||||
} else {
|
||||
parameterName = "arg" + strconv.Itoa(i)
|
||||
}
|
||||
|
||||
count := parameterNameCounts[parameterName]
|
||||
parameterNameCounts[parameterName] = count + 1
|
||||
|
||||
if count > 0 {
|
||||
parameterName += strconv.Itoa(count)
|
||||
}
|
||||
|
||||
var questionToken *ast.QuestionToken
|
||||
if i >= minArgumentCount {
|
||||
questionToken = factory.NewToken(ast.KindQuestionToken)
|
||||
}
|
||||
|
||||
var typeNode *ast.TypeNode
|
||||
if inJS {
|
||||
typeNode = nil
|
||||
} else if i < len(types) && types[i] != nil {
|
||||
typeNode = types[i]
|
||||
} else {
|
||||
typeNode = factory.NewKeywordTypeNode(ast.KindUnknownKeyword)
|
||||
}
|
||||
parameters = append(parameters,
|
||||
factory.NewParameterDeclaration(nil /*modifiers*/, nil /*dotDotDotToken*/, factory.NewIdentifier(parameterName), questionToken, typeNode, nil /*initializer*/))
|
||||
}
|
||||
return factory.NewNodeList(parameters)
|
||||
}
|
||||
|
||||
func createDeclarationName(factory *ast.NodeFactory, typeChecker *checker.Checker, symbol *ast.Symbol, declaration *ast.Node) *ast.PropertyName {
|
||||
if symbol != nil && symbol.CheckFlags&ast.CheckFlagsMapped != 0 {
|
||||
nameType := typeChecker.GetNameTypeOfSymbol(symbol)
|
||||
if nameType != nil && checker.IsTypeUsableAsPropertyName(nameType) {
|
||||
return factory.NewIdentifier(checker.GetPropertyNameFromType(nameType))
|
||||
}
|
||||
}
|
||||
if declaration != nil && declaration.Name() != nil {
|
||||
return declaration.Name().Clone(factory)
|
||||
}
|
||||
if symbol != nil {
|
||||
return factory.NewIdentifier(symbol.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createPropertyName(factory *ast.NodeFactory, node *ast.Node, quotePreference lsutil.QuotePreference) *ast.PropertyName {
|
||||
if ast.IsIdentifier(node) && node.Text() == "constructor" {
|
||||
tokenFlags := ast.TokenFlagsNone
|
||||
if quotePreference == lsutil.QuotePreferenceSingle {
|
||||
tokenFlags = ast.TokenFlagsSingleQuote
|
||||
}
|
||||
return factory.NewComputedPropertyName(factory.NewStringLiteral(node.Text(), tokenFlags))
|
||||
}
|
||||
return factory.DeepCloneNode(node)
|
||||
}
|
||||
207
tools/tsgo/internal/ls/codelens.go
Normal file
207
tools/tsgo/internal/ls/codelens.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
func (l *LanguageService) ProvideCodeLenses(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.CodeLensResponse, error) {
|
||||
_, file := l.getProgramAndFile(documentURI)
|
||||
|
||||
userPrefs := l.UserPreferences().CodeLens
|
||||
if !userPrefs.ReferencesCodeLensEnabled.IsTrue() && !userPrefs.ImplementationsCodeLensEnabled.IsTrue() {
|
||||
return lsproto.CodeLensResponse{}, nil
|
||||
}
|
||||
|
||||
// Keeps track of the last symbol to avoid duplicating code lenses across overloads.
|
||||
var lastSymbol *ast.Symbol
|
||||
var result []*lsproto.CodeLens
|
||||
var visit func(node *ast.Node) bool
|
||||
visit = func(node *ast.Node) bool {
|
||||
if ctx.Err() != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if currentSymbol := node.Symbol(); lastSymbol != currentSymbol {
|
||||
lastSymbol = currentSymbol
|
||||
|
||||
if userPrefs.ReferencesCodeLensEnabled.IsTrue() && isValidReferenceLensNode(node, userPrefs) {
|
||||
result = append(result, l.newCodeLensForNode(documentURI, file, node, lsproto.CodeLensKindReferences))
|
||||
}
|
||||
|
||||
if userPrefs.ImplementationsCodeLensEnabled.IsTrue() && isValidImplementationsCodeLensNode(node, userPrefs) {
|
||||
result = append(result, l.newCodeLensForNode(documentURI, file, node, lsproto.CodeLensKindImplementations))
|
||||
}
|
||||
}
|
||||
|
||||
savedLastSymbol := lastSymbol
|
||||
node.ForEachChild(visit)
|
||||
lastSymbol = savedLastSymbol
|
||||
return false
|
||||
}
|
||||
|
||||
visit(file.AsNode())
|
||||
|
||||
return lsproto.CodeLensResponse{
|
||||
CodeLenses: &result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) ResolveCodeLens(ctx context.Context, codeLens *lsproto.CodeLens, showLocationsCommandName *string, orchestrator CrossProjectOrchestrator) (*lsproto.CodeLens, error) {
|
||||
uri := codeLens.Data.Uri
|
||||
textDoc := lsproto.TextDocumentIdentifier{Uri: uri}
|
||||
locale := locale.FromContext(ctx)
|
||||
var locs []lsproto.Location
|
||||
var lensTitle string
|
||||
switch codeLens.Data.Kind {
|
||||
case lsproto.CodeLensKindReferences:
|
||||
referencesResp, err := l.ProvideReferences(ctx, &lsproto.ReferenceParams{
|
||||
TextDocument: textDoc,
|
||||
Position: codeLens.Range.Start,
|
||||
Context: &lsproto.ReferenceContext{
|
||||
// Don't include the declaration in the references count.
|
||||
IncludeDeclaration: false,
|
||||
},
|
||||
}, orchestrator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if referencesResp.Locations != nil {
|
||||
locs = *referencesResp.Locations
|
||||
}
|
||||
|
||||
if len(locs) == 1 {
|
||||
lensTitle = diagnostics.X_1_reference.Localize(locale)
|
||||
} else {
|
||||
lensTitle = diagnostics.X_0_references.Localize(locale, len(locs))
|
||||
}
|
||||
case lsproto.CodeLensKindImplementations:
|
||||
|
||||
implementations, err := l.provideImplementationsEx(
|
||||
ctx,
|
||||
&lsproto.ImplementationParams{
|
||||
TextDocument: textDoc,
|
||||
Position: codeLens.Range.Start,
|
||||
},
|
||||
// "Force" link support to be false so that we only get `Locations` back,
|
||||
// and don't include the "current" node in the results.
|
||||
symbolEntryTransformOptions{
|
||||
requireLocationsResult: true,
|
||||
dropOriginNodes: true,
|
||||
},
|
||||
orchestrator,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if implementations.Locations != nil {
|
||||
locs = *implementations.Locations
|
||||
}
|
||||
|
||||
if len(locs) == 1 {
|
||||
lensTitle = diagnostics.X_1_implementation.Localize(locale)
|
||||
} else {
|
||||
lensTitle = diagnostics.X_0_implementations.Localize(locale, len(locs))
|
||||
}
|
||||
}
|
||||
|
||||
cmd := &lsproto.Command{
|
||||
Title: lensTitle,
|
||||
}
|
||||
if len(locs) > 0 && showLocationsCommandName != nil {
|
||||
cmd.Command = *showLocationsCommandName
|
||||
cmd.Arguments = &[]any{
|
||||
uri,
|
||||
codeLens.Range.Start,
|
||||
locs,
|
||||
}
|
||||
}
|
||||
|
||||
codeLens.Command = cmd
|
||||
return codeLens, nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) newCodeLensForNode(fileUri lsproto.DocumentUri, file *ast.SourceFile, node *ast.Node, kind lsproto.CodeLensKind) *lsproto.CodeLens {
|
||||
nodeForRange := node
|
||||
nodeName := node.Name()
|
||||
if nodeName != nil {
|
||||
nodeForRange = nodeName
|
||||
}
|
||||
pos := scanner.SkipTrivia(file.Text(), nodeForRange.Pos())
|
||||
|
||||
return &lsproto.CodeLens{
|
||||
Range: lsproto.Range{
|
||||
Start: l.converters.PositionToLineAndCharacter(file, core.TextPos(pos)),
|
||||
End: l.converters.PositionToLineAndCharacter(file, core.TextPos(node.End())),
|
||||
},
|
||||
Data: &lsproto.CodeLensData{
|
||||
Kind: kind,
|
||||
Uri: fileUri,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func isValidImplementationsCodeLensNode(node *ast.Node, userPrefs lsutil.CodeLensUserPreferences) bool {
|
||||
switch node.Kind {
|
||||
// Always show on interfaces
|
||||
case ast.KindInterfaceDeclaration:
|
||||
// TODO: ast.KindTypeAliasDeclaration?
|
||||
return true
|
||||
|
||||
// If configured, show on interface methods
|
||||
case ast.KindMethodSignature:
|
||||
return userPrefs.ImplementationsCodeLensShowOnInterfaceMethods.IsTrue() && node.Parent.Kind == ast.KindInterfaceDeclaration
|
||||
|
||||
// If configured, show on all class methods - but not private ones.
|
||||
case ast.KindMethodDeclaration:
|
||||
if userPrefs.ImplementationsCodeLensShowOnAllClassMethods.IsTrue() && node.Parent.Kind == ast.KindClassDeclaration {
|
||||
return !ast.HasModifier(node, ast.ModifierFlagsPrivate) && node.Name().Kind != ast.KindPrivateIdentifier
|
||||
}
|
||||
fallthrough
|
||||
|
||||
// Always show on abstract classes/properties/methods
|
||||
case ast.KindClassDeclaration, ast.KindConstructor,
|
||||
ast.KindGetAccessor, ast.KindSetAccessor, ast.KindPropertyDeclaration:
|
||||
return ast.HasModifier(node, ast.ModifierFlagsAbstract)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isValidReferenceLensNode(node *ast.Node, userPrefs lsutil.CodeLensUserPreferences) bool {
|
||||
switch node.Kind {
|
||||
case ast.KindFunctionDeclaration:
|
||||
if userPrefs.ReferencesCodeLensShowOnAllFunctions.IsTrue() {
|
||||
return true
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case ast.KindVariableDeclaration:
|
||||
return ast.GetCombinedModifierFlags(node)&ast.ModifierFlagsExport != 0
|
||||
|
||||
case ast.KindClassDeclaration, ast.KindInterfaceDeclaration, ast.KindTypeAliasDeclaration, ast.KindEnumDeclaration, ast.KindEnumMember:
|
||||
return true
|
||||
|
||||
case ast.KindMethodDeclaration, ast.KindMethodSignature, ast.KindConstructor,
|
||||
ast.KindGetAccessor, ast.KindSetAccessor,
|
||||
ast.KindPropertyDeclaration, ast.KindPropertySignature:
|
||||
// Don't show if child and parent have same start
|
||||
// For https://github.com/microsoft/vscode/issues/90396
|
||||
// !!!
|
||||
|
||||
switch node.Parent.Kind {
|
||||
case ast.KindClassDeclaration, ast.KindInterfaceDeclaration, ast.KindTypeLiteral:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
6295
tools/tsgo/internal/ls/completions.go
Normal file
6295
tools/tsgo/internal/ls/completions.go
Normal file
File diff suppressed because it is too large
Load Diff
6
tools/tsgo/internal/ls/constants.go
Normal file
6
tools/tsgo/internal/ls/constants.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package ls
|
||||
|
||||
const (
|
||||
moduleSpecifierResolutionLimit = 100
|
||||
moduleSpecifierResolutionCacheAttemptLimit = 1000
|
||||
)
|
||||
421
tools/tsgo/internal/ls/crossproject.go
Normal file
421
tools/tsgo/internal/ls/crossproject.go
Normal file
@@ -0,0 +1,421 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
|
||||
"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/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
type Project interface {
|
||||
Id() tspath.Path
|
||||
GetProgram() *compiler.Program
|
||||
HasFile(fileName string) bool
|
||||
}
|
||||
|
||||
type projectAndTextDocumentPosition struct {
|
||||
project Project
|
||||
ls *LanguageService
|
||||
Uri lsproto.DocumentUri
|
||||
Position lsproto.Position
|
||||
forOriginalLocation bool
|
||||
}
|
||||
|
||||
type response[Resp any] struct {
|
||||
complete bool
|
||||
result Resp
|
||||
forOriginalLocation bool
|
||||
}
|
||||
|
||||
type CrossProjectOrchestrator interface {
|
||||
GetDefaultProject() Project
|
||||
GetAllProjectsForInitialRequest() []Project
|
||||
GetLanguageServiceForProjectWithFile(ctx context.Context, project Project, uri lsproto.DocumentUri) *LanguageService
|
||||
GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUri) ([]Project, error)
|
||||
GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.Path]) iter.Seq[Project]
|
||||
}
|
||||
|
||||
func handleCrossProject[Req lsproto.HasTextDocumentPosition, Resp any](
|
||||
defaultLs *LanguageService,
|
||||
ctx context.Context,
|
||||
params Req,
|
||||
orchestrator CrossProjectOrchestrator,
|
||||
symbolAndEntriesToResp func(*LanguageService, context.Context, Req, SymbolAndEntriesData, symbolEntryTransformOptions) (Resp, error),
|
||||
combineResults func(iter.Seq[Resp]) Resp,
|
||||
isRename bool,
|
||||
implementations bool,
|
||||
options symbolEntryTransformOptions,
|
||||
) (Resp, error) {
|
||||
var resp Resp
|
||||
var err error
|
||||
|
||||
// Single project
|
||||
if orchestrator == nil {
|
||||
data, _ := defaultLs.provideSymbolsAndEntries(ctx, params.TextDocumentURI(), params.TextDocumentPosition(), isRename, implementations)
|
||||
return symbolAndEntriesToResp(defaultLs, ctx, params, data, options)
|
||||
}
|
||||
|
||||
defaultProject := orchestrator.GetDefaultProject()
|
||||
allProjects := orchestrator.GetAllProjectsForInitialRequest()
|
||||
var results collections.SyncMap[tspath.Path, *response[Resp]]
|
||||
var defaultDefinition *nonLocalDefinition
|
||||
canSearchProject := func(project Project) bool {
|
||||
_, searched := results.Load(project.Id())
|
||||
return !searched
|
||||
}
|
||||
wg := core.NewWorkGroup(false)
|
||||
var errMu sync.Mutex
|
||||
var enqueueItem func(item projectAndTextDocumentPosition)
|
||||
var panicsOccured []string
|
||||
var panicMu sync.Mutex
|
||||
enqueueItem = func(item projectAndTextDocumentPosition) {
|
||||
var response response[Resp]
|
||||
if _, loaded := results.LoadOrStore(item.project.Id(), &response); loaded {
|
||||
return
|
||||
}
|
||||
wg.Queue(func() {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stack := debug.Stack()
|
||||
panicOccured := fmt.Sprintf("panic handling request: %v\n%s", r, string(stack))
|
||||
panicMu.Lock()
|
||||
panicsOccured = append(panicsOccured, panicOccured)
|
||||
panicMu.Unlock()
|
||||
}
|
||||
}()
|
||||
// Process the item
|
||||
ls := item.ls
|
||||
if ls == nil {
|
||||
// Get it now
|
||||
ls = orchestrator.GetLanguageServiceForProjectWithFile(ctx, item.project, item.Uri)
|
||||
if ls == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
data, ok := ls.provideSymbolsAndEntries(ctx, item.Uri, item.Position, isRename, implementations)
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
for _, entry := range data.SymbolsAndEntries {
|
||||
// Find the default definition that can be in another project
|
||||
// Later we will use this load ancestor tree that references this location and expand search
|
||||
if item.project == defaultProject && defaultDefinition == nil {
|
||||
defaultDefinition = ls.getNonLocalDefinition(ctx, entry)
|
||||
}
|
||||
ls.forEachOriginalDefinitionLocation(ctx, entry, func(uri lsproto.DocumentUri, position lsproto.Position) {
|
||||
// Get default configured project for this file
|
||||
defProjects, errProjects := orchestrator.GetProjectsForFile(ctx, uri)
|
||||
if errProjects != nil {
|
||||
return
|
||||
}
|
||||
for _, defProject := range defProjects {
|
||||
// Optimization: don't enqueue if will be discarded
|
||||
if canSearchProject(defProject) {
|
||||
enqueueItem(projectAndTextDocumentPosition{
|
||||
project: defProject,
|
||||
Uri: uri,
|
||||
Position: position,
|
||||
forOriginalLocation: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if result, errSearch := symbolAndEntriesToResp(ls, ctx, params, data, options); errSearch == nil {
|
||||
response.complete = true
|
||||
response.result = result
|
||||
response.forOriginalLocation = item.forOriginalLocation
|
||||
} else {
|
||||
errMu.Lock()
|
||||
defer errMu.Unlock()
|
||||
if err == nil {
|
||||
err = errSearch
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Initial set of projects and locations in the queue, starting with default project
|
||||
enqueueItem(projectAndTextDocumentPosition{
|
||||
project: defaultProject,
|
||||
ls: defaultLs,
|
||||
Uri: params.TextDocumentURI(),
|
||||
Position: params.TextDocumentPosition(),
|
||||
})
|
||||
for _, project := range allProjects {
|
||||
if project != defaultProject {
|
||||
enqueueItem(projectAndTextDocumentPosition{
|
||||
project: project,
|
||||
// TODO!! symlinks need to change the URI
|
||||
Uri: params.TextDocumentURI(),
|
||||
Position: params.TextDocumentPosition(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
getResultsIterator := func() iter.Seq[Resp] {
|
||||
return func(yield func(Resp) bool) {
|
||||
var seenProjects collections.SyncSet[tspath.Path]
|
||||
if response, loaded := results.Load(defaultProject.Id()); loaded && response.complete {
|
||||
if !yield(response.result) {
|
||||
return
|
||||
}
|
||||
}
|
||||
seenProjects.Add(defaultProject.Id())
|
||||
for _, project := range allProjects {
|
||||
if seenProjects.AddIfAbsent(project.Id()) {
|
||||
if response, loaded := results.Load(project.Id()); loaded && response.complete {
|
||||
if !yield(response.result) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Prefer the searches from locations for default definition
|
||||
results.Range(func(key tspath.Path, response *response[Resp]) bool {
|
||||
if !response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete {
|
||||
return yield(response.result)
|
||||
}
|
||||
return true
|
||||
})
|
||||
// Then the searches from original locations
|
||||
results.Range(func(key tspath.Path, response *response[Resp]) bool {
|
||||
if response.forOriginalLocation && seenProjects.AddIfAbsent(key) && response.complete {
|
||||
return yield(response.result)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Outer loop - to complete work if more is added after completing existing queue
|
||||
for {
|
||||
// Process existing known projects first
|
||||
wg.RunAndWait()
|
||||
// No need to use mu here since we are not in parallel at this point
|
||||
if panicsOccured != nil {
|
||||
panic(fmt.Sprintf("Panics occurred during cross-project handling: %v", panicsOccured))
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return resp, ctx.Err()
|
||||
}
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
wg = core.NewWorkGroup(false)
|
||||
hasMoreWork := false
|
||||
if defaultDefinition != nil {
|
||||
var requestedProjectTrees collections.Set[tspath.Path]
|
||||
results.Range(func(key tspath.Path, response *response[Resp]) bool {
|
||||
if response.complete {
|
||||
requestedProjectTrees.Add(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// Load more projects based on default definition found
|
||||
for loadedProject := range orchestrator.GetProjectsLoadingProjectTree(ctx, &requestedProjectTrees) {
|
||||
if ctx.Err() != nil {
|
||||
return resp, ctx.Err()
|
||||
}
|
||||
|
||||
// Can loop forever without this (enqueue here, dequeue above, repeat)
|
||||
if !canSearchProject(loadedProject) || loadedProject.GetProgram() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Enqueue the project and location for further processing
|
||||
if loadedProject.HasFile(defaultDefinition.TextDocumentURI().FileName()) {
|
||||
enqueueItem(projectAndTextDocumentPosition{
|
||||
project: loadedProject,
|
||||
Uri: defaultDefinition.TextDocumentURI(),
|
||||
Position: defaultDefinition.TextDocumentPosition(),
|
||||
})
|
||||
hasMoreWork = true
|
||||
} else if sourcePos := defaultDefinition.GetSourcePosition(); sourcePos != nil && loadedProject.HasFile(sourcePos.TextDocumentURI().FileName()) {
|
||||
enqueueItem(projectAndTextDocumentPosition{
|
||||
project: loadedProject,
|
||||
Uri: sourcePos.TextDocumentURI(),
|
||||
Position: sourcePos.TextDocumentPosition(),
|
||||
})
|
||||
hasMoreWork = true
|
||||
} else if generatedPos := defaultDefinition.GetGeneratedPosition(); generatedPos != nil && loadedProject.HasFile(generatedPos.TextDocumentURI().FileName()) {
|
||||
enqueueItem(projectAndTextDocumentPosition{
|
||||
project: loadedProject,
|
||||
Uri: generatedPos.TextDocumentURI(),
|
||||
Position: generatedPos.TextDocumentPosition(),
|
||||
})
|
||||
hasMoreWork = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasMoreWork {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if results.Size() > 1 {
|
||||
resp = combineResults(getResultsIterator())
|
||||
} else {
|
||||
// Single result, return that directly
|
||||
for value := range getResultsIterator() {
|
||||
resp = value
|
||||
break
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func combineLocationArray[T lsproto.HasLocation](
|
||||
combined []T,
|
||||
locations *[]T,
|
||||
seen *collections.Set[lsproto.Location],
|
||||
) []T {
|
||||
for _, loc := range *locations {
|
||||
if seen.AddIfAbsent(loc.GetLocation()) {
|
||||
combined = append(combined, loc)
|
||||
}
|
||||
}
|
||||
return combined
|
||||
}
|
||||
|
||||
func combineResponseLocations[T lsproto.HasLocations](results iter.Seq[T]) *[]lsproto.Location {
|
||||
var combined []lsproto.Location
|
||||
var seenLocations collections.Set[lsproto.Location]
|
||||
for resp := range results {
|
||||
if locations := resp.GetLocations(); locations != nil {
|
||||
combined = combineLocationArray(combined, locations, &seenLocations)
|
||||
}
|
||||
}
|
||||
return &combined
|
||||
}
|
||||
|
||||
func combineReferences(results iter.Seq[lsproto.ReferencesResponse]) lsproto.ReferencesResponse {
|
||||
return lsproto.LocationsOrNull{Locations: combineResponseLocations(results)}
|
||||
}
|
||||
|
||||
func combineVSReferences(results iter.Seq[lsproto.VSReferencesResponse]) lsproto.VSReferencesResponse {
|
||||
var combined []*lsproto.VSReferenceItem
|
||||
// Re-number IDs across projects to maintain unique IDs and correct definition references
|
||||
nextId := int32(0)
|
||||
for resp := range results {
|
||||
if resp.VSReferenceItems == nil {
|
||||
continue
|
||||
}
|
||||
// Map old IDs to new IDs for this batch
|
||||
idMap := make(map[int32]int32)
|
||||
for _, item := range *resp.VSReferenceItems {
|
||||
oldId := item.VSId
|
||||
newId := nextId
|
||||
idMap[oldId] = newId
|
||||
nextId++
|
||||
|
||||
newItem := *item
|
||||
newItem.VSId = newId
|
||||
if item.VSDefinitionId != nil {
|
||||
newDefId := idMap[*item.VSDefinitionId]
|
||||
newItem.VSDefinitionId = &newDefId
|
||||
}
|
||||
combined = append(combined, &newItem)
|
||||
}
|
||||
}
|
||||
return lsproto.VSReferencesResponse{VSReferenceItems: &combined}
|
||||
}
|
||||
|
||||
func combineImplementations(results iter.Seq[lsproto.ImplementationResponse]) lsproto.ImplementationResponse {
|
||||
var combined []*lsproto.LocationLink
|
||||
var seenLocations collections.Set[lsproto.Location]
|
||||
for resp := range results {
|
||||
if definitionLinks := resp.DefinitionLinks; definitionLinks != nil {
|
||||
combined = combineLocationArray(combined, definitionLinks, &seenLocations)
|
||||
} else if locations := resp.Locations; locations != nil {
|
||||
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: combineResponseLocations(results)}
|
||||
}
|
||||
}
|
||||
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &combined}
|
||||
}
|
||||
|
||||
func combineRenameResponse(results iter.Seq[lsproto.RenameResponse]) lsproto.RenameResponse {
|
||||
combined := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
|
||||
seenChanges := make(map[lsproto.DocumentUri]*collections.Set[lsproto.Range])
|
||||
var documentChanges []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
|
||||
seenRenames := collections.Set[[2]lsproto.DocumentUri]{}
|
||||
|
||||
for resp := range results {
|
||||
if resp.WorkspaceEdit != nil && resp.WorkspaceEdit.DocumentChanges != nil {
|
||||
for _, change := range *resp.WorkspaceEdit.DocumentChanges {
|
||||
switch {
|
||||
case change.RenameFile != nil:
|
||||
key := [2]lsproto.DocumentUri{change.RenameFile.OldUri, change.RenameFile.NewUri}
|
||||
if seenRenames.AddIfAbsent(key) {
|
||||
documentChanges = append(documentChanges, change)
|
||||
}
|
||||
default:
|
||||
documentChanges = append(documentChanges, change)
|
||||
}
|
||||
}
|
||||
}
|
||||
if resp.WorkspaceEdit != nil && resp.WorkspaceEdit.Changes != nil {
|
||||
for doc, changes := range *resp.WorkspaceEdit.Changes {
|
||||
seenSet, ok := seenChanges[doc]
|
||||
if !ok {
|
||||
seenSet = &collections.Set[lsproto.Range]{}
|
||||
seenChanges[doc] = seenSet
|
||||
}
|
||||
changesForDoc, exists := combined[doc]
|
||||
if !exists {
|
||||
changesForDoc = []*lsproto.TextEdit{}
|
||||
}
|
||||
for _, change := range changes {
|
||||
if !seenSet.Has(change.Range) {
|
||||
seenSet.Add(change.Range)
|
||||
changesForDoc = append(changesForDoc, change)
|
||||
}
|
||||
}
|
||||
combined[doc] = changesForDoc
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(documentChanges) > 0 || len(combined) > 0 {
|
||||
workspaceEdit := &lsproto.WorkspaceEdit{}
|
||||
if len(documentChanges) > 0 {
|
||||
workspaceEdit.DocumentChanges = &documentChanges
|
||||
}
|
||||
if len(combined) > 0 {
|
||||
workspaceEdit.Changes = &combined
|
||||
}
|
||||
return lsproto.RenameResponse{
|
||||
WorkspaceEdit: workspaceEdit,
|
||||
}
|
||||
}
|
||||
return lsproto.RenameResponse{}
|
||||
}
|
||||
|
||||
func combineIncomingCalls(results iter.Seq[lsproto.CallHierarchyIncomingCallsResponse]) lsproto.CallHierarchyIncomingCallsResponse {
|
||||
var combined []*lsproto.CallHierarchyIncomingCall
|
||||
var seenCalls collections.Set[lsproto.Location]
|
||||
for resp := range results {
|
||||
if resp.CallHierarchyIncomingCalls != nil {
|
||||
for _, call := range *resp.CallHierarchyIncomingCalls {
|
||||
if seenCalls.AddIfAbsent(call.From.GetLocation()) {
|
||||
combined = append(combined, call)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lsproto.CallHierarchyIncomingCallsResponse{CallHierarchyIncomingCalls: &combined}
|
||||
}
|
||||
440
tools/tsgo/internal/ls/definition.go
Normal file
440
tools/tsgo/internal/ls/definition.go
Normal file
@@ -0,0 +1,440 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"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/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
func (l *LanguageService) ProvideDefinition(
|
||||
ctx context.Context,
|
||||
documentURI lsproto.DocumentUri,
|
||||
position lsproto.Position,
|
||||
) (lsproto.DefinitionResponse, error) {
|
||||
if l.UserPreferences().PreferGoToSourceDefinition {
|
||||
return l.ProvideSourceDefinition(ctx, documentURI, position)
|
||||
}
|
||||
return l.provideDefinitionWorker(ctx, documentURI, position)
|
||||
}
|
||||
|
||||
func (l *LanguageService) provideDefinitionWorker(
|
||||
ctx context.Context,
|
||||
documentURI lsproto.DocumentUri,
|
||||
position lsproto.Position,
|
||||
) (lsproto.DefinitionResponse, error) {
|
||||
caps := lsproto.GetClientCapabilities(ctx)
|
||||
clientSupportsLink := caps.TextDocument.Definition.LinkSupport
|
||||
|
||||
program, file := l.getProgramAndFile(documentURI)
|
||||
pos := int(l.converters.LineAndCharacterToPosition(file, position))
|
||||
node := astnav.GetTouchingPropertyName(file, pos)
|
||||
reference := getReferenceAtPosition(file, pos, program)
|
||||
|
||||
if node.Kind == ast.KindSourceFile {
|
||||
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil
|
||||
}
|
||||
|
||||
originSelectionRange := l.createLspRangeFromNode(node, file)
|
||||
if reference != nil && reference.file != nil {
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, []*ast.Node{}, reference), nil
|
||||
}
|
||||
|
||||
c, done := program.GetTypeCheckerForFile(ctx, file)
|
||||
defer done()
|
||||
|
||||
if node.Kind == ast.KindOverrideKeyword {
|
||||
if sym := getSymbolForOverriddenMember(c, node); sym != nil {
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, sym.Declarations, nil /*reference*/), nil
|
||||
}
|
||||
}
|
||||
|
||||
if ast.IsJumpStatementTarget(node) {
|
||||
if label := getTargetLabel(node.Parent, node.Text()); label != nil {
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, []*ast.Node{label}, nil /*reference*/), nil
|
||||
}
|
||||
}
|
||||
|
||||
if node.Kind == ast.KindCaseKeyword || node.Kind == ast.KindDefaultKeyword && ast.IsDefaultClause(node.Parent) {
|
||||
if stmt := ast.FindAncestor(node.Parent, ast.IsSwitchStatement); stmt != nil {
|
||||
file := ast.GetSourceFileOfNode(stmt)
|
||||
return l.createLocationFromFileAndRange(file, scanner.GetRangeOfTokenAtPosition(file, stmt.Pos())), nil
|
||||
}
|
||||
}
|
||||
|
||||
if node.Kind == ast.KindReturnKeyword || node.Kind == ast.KindYieldKeyword || node.Kind == ast.KindAwaitKeyword {
|
||||
if fn := ast.FindAncestor(node, ast.IsFunctionLikeDeclaration); fn != nil {
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, []*ast.Node{fn}, nil /*reference*/), nil
|
||||
}
|
||||
}
|
||||
|
||||
declarations := getDeclarationsFromLocation(c, node)
|
||||
calledDeclaration := tryGetSignatureDeclaration(c, node)
|
||||
if calledDeclaration != nil && !(ast.IsJsxOpeningLikeElement(node.Parent) && isJsxConstructorLike(calledDeclaration)) {
|
||||
symbol := c.GetSymbolAtLocation(getDeclarationNameForKeyword(node))
|
||||
if symbol != nil && core.Some(c.GetRootSymbols(symbol), func(rootSymbol *ast.Symbol) bool {
|
||||
return symbolMatchesSignature(rootSymbol, calledDeclaration)
|
||||
}) {
|
||||
if !ast.IsConstructorDeclaration(calledDeclaration) {
|
||||
declarations = nil
|
||||
} else {
|
||||
declarations = core.Filter(slices.Clip(declarations), func(node *ast.Node) bool {
|
||||
return node != calledDeclaration && (ast.IsClassDeclaration(node) || ast.IsClassExpression(node))
|
||||
})
|
||||
}
|
||||
} else {
|
||||
declarations = core.Filter(slices.Clip(declarations), func(node *ast.Node) bool { return node != calledDeclaration })
|
||||
}
|
||||
declarations = append(declarations, calledDeclaration)
|
||||
}
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, declarations, reference), nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) ProvideTypeDefinition(
|
||||
ctx context.Context,
|
||||
documentURI lsproto.DocumentUri,
|
||||
position lsproto.Position,
|
||||
) (lsproto.TypeDefinitionResponse, error) {
|
||||
caps := lsproto.GetClientCapabilities(ctx)
|
||||
clientSupportsLink := caps.TextDocument.TypeDefinition.LinkSupport
|
||||
|
||||
program, file := l.getProgramAndFile(documentURI)
|
||||
node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position)))
|
||||
if node.Kind == ast.KindSourceFile {
|
||||
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil
|
||||
}
|
||||
originSelectionRange := l.createLspRangeFromNode(node, file)
|
||||
|
||||
c, done := program.GetTypeCheckerForFile(ctx, file)
|
||||
defer done()
|
||||
|
||||
node = getDeclarationNameForKeyword(node)
|
||||
|
||||
if symbol := c.GetSymbolAtLocation(node); symbol != nil {
|
||||
symbolType := getTypeOfSymbolAtLocation(c, symbol, node)
|
||||
declarations := getDeclarationsFromType(symbolType)
|
||||
if typeArgument := c.GetFirstTypeArgumentFromKnownType(symbolType); typeArgument != nil {
|
||||
declarations = core.Concatenate(getDeclarationsFromType(typeArgument), declarations)
|
||||
}
|
||||
if len(declarations) != 0 {
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, declarations, nil /*reference*/), nil
|
||||
}
|
||||
if symbol.Flags&ast.SymbolFlagsValue == 0 && symbol.Flags&ast.SymbolFlagsType != 0 {
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, symbol.Declarations, nil /*reference*/), nil
|
||||
}
|
||||
}
|
||||
|
||||
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil
|
||||
}
|
||||
|
||||
func getDeclarationNameForKeyword(node *ast.Node) *ast.Node {
|
||||
if node.Kind >= ast.KindFirstKeyword && node.Kind <= ast.KindLastKeyword {
|
||||
if ast.IsVariableDeclarationList(node.Parent) {
|
||||
if decl := core.FirstOrNil(node.Parent.AsVariableDeclarationList().Declarations.Nodes); decl != nil && decl.Name() != nil {
|
||||
return decl.Name()
|
||||
}
|
||||
} else if node.Parent.DeclarationData() != nil && node.Parent.Name() != nil && node.Pos() < node.Parent.Name().Pos() {
|
||||
return node.Parent.Name()
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
type fileRange struct {
|
||||
fileName string
|
||||
fileRange core.TextRange
|
||||
}
|
||||
|
||||
func (l *LanguageService) createDefinitionLocations(
|
||||
originSelectionRange lsproto.Range,
|
||||
clientSupportsLink bool,
|
||||
declarations []*ast.Node,
|
||||
reference *refInfo,
|
||||
) lsproto.DefinitionResponse {
|
||||
locations := make([]*lsproto.LocationLink, 0)
|
||||
locationRanges := collections.Set[fileRange]{}
|
||||
|
||||
if reference != nil {
|
||||
targetRange := lsproto.Range{
|
||||
Start: lsproto.Position{
|
||||
Line: 0,
|
||||
Character: 0,
|
||||
},
|
||||
End: lsproto.Position{
|
||||
Line: 0,
|
||||
Character: 0,
|
||||
},
|
||||
}
|
||||
locations = append(locations, &lsproto.LocationLink{
|
||||
OriginSelectionRange: &originSelectionRange,
|
||||
TargetUri: lsconv.FileNameToDocumentURI(reference.fileName),
|
||||
TargetRange: targetRange,
|
||||
TargetSelectionRange: targetRange,
|
||||
})
|
||||
}
|
||||
|
||||
for _, decl := range declarations {
|
||||
file := ast.GetSourceFileOfNode(decl)
|
||||
fileName := file.FileName()
|
||||
name := core.OrElse(ast.GetNameOfDeclaration(decl), decl)
|
||||
var nameRange core.TextRange
|
||||
if name.Kind == ast.KindEmptyStatement {
|
||||
nameRange = core.NewTextRange(name.Pos(), name.Pos())
|
||||
} else {
|
||||
nameRange = createRangeFromNode(name, file)
|
||||
}
|
||||
if locationRanges.AddIfAbsent(fileRange{fileName, nameRange}) {
|
||||
contextNode := core.OrElse(getContextNode(decl), decl)
|
||||
contextRange := core.OrElse(toContextRange(&nameRange, file, contextNode), &nameRange)
|
||||
targetSelectionLoc := l.getMappedLocation(fileName, nameRange)
|
||||
targetLoc := l.getMappedLocation(fileName, *contextRange)
|
||||
locations = append(locations, &lsproto.LocationLink{
|
||||
OriginSelectionRange: &originSelectionRange,
|
||||
TargetSelectionRange: targetSelectionLoc.Range,
|
||||
TargetUri: targetLoc.Uri,
|
||||
TargetRange: targetLoc.Range,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if clientSupportsLink {
|
||||
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &locations}
|
||||
}
|
||||
return createLocationsFromLinks(locations)
|
||||
}
|
||||
|
||||
func createLocationsFromLinks(links []*lsproto.LocationLink) lsproto.DefinitionResponse {
|
||||
locations := core.Map(links, func(link *lsproto.LocationLink) lsproto.Location {
|
||||
return lsproto.Location{
|
||||
Uri: link.TargetUri,
|
||||
Range: link.TargetSelectionRange,
|
||||
}
|
||||
})
|
||||
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations}
|
||||
}
|
||||
|
||||
func (l *LanguageService) createLocationFromFileAndRange(file *ast.SourceFile, textRange core.TextRange) lsproto.DefinitionResponse {
|
||||
mappedLocation := l.getMappedLocation(file.FileName(), textRange)
|
||||
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{
|
||||
Location: &mappedLocation,
|
||||
}
|
||||
}
|
||||
|
||||
func getDeclarationsFromLocation(c *checker.Checker, node *ast.Node) []*ast.Node {
|
||||
if ast.IsIdentifier(node) && ast.IsShorthandPropertyAssignment(node.Parent) {
|
||||
// Because name in short-hand property assignment has two different meanings: property name and property value,
|
||||
// using go-to-definition at such position should go to the variable declaration of the property value rather than
|
||||
// go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition
|
||||
// is performed at the location of property access, we would like to go to definition of the property in the short-hand
|
||||
// assignment. This case and others are handled by the following code.
|
||||
// and the contextual type's property declarations
|
||||
shorthandSymbol := c.GetResolvedSymbol(node)
|
||||
var declarations []*ast.Node
|
||||
if shorthandSymbol != nil {
|
||||
declarations = shorthandSymbol.Declarations
|
||||
}
|
||||
contextualDeclarations := getDeclarationsFromObjectLiteralElement(c, node)
|
||||
return core.Concatenate(declarations, contextualDeclarations)
|
||||
}
|
||||
|
||||
if ast.IsPropertyName(node) && ast.IsBindingElement(node.Parent) && ast.IsObjectBindingPattern(node.Parent.Parent) {
|
||||
// If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the
|
||||
// declaration of the symbol (which is itself), we should try to get to the original type of the
|
||||
// ObjectBindingPattern and return the property declaration for the referenced property.
|
||||
// For example:
|
||||
// import('./foo').then(({ bar }) => undefined); => should navigate to the declaration in file "./foo"
|
||||
//
|
||||
// function bar<T>(onfulfilled: (value: T) => void) { }
|
||||
// interface Test { prop1: number }
|
||||
// bar<Test>(({ prop1 }) => {}); => should navigate to prop1 in Test
|
||||
bindingEl := node.Parent.AsBindingElement()
|
||||
if bindingEl.DotDotDotToken == nil && node == core.OrElse(bindingEl.PropertyName, node.Parent.Name()) {
|
||||
if name, ok := ast.TryGetTextOfPropertyName(node); ok {
|
||||
t := c.GetTypeAtLocation(node.Parent.Parent)
|
||||
types := []*checker.Type{t}
|
||||
if t.IsUnion() {
|
||||
types = t.Types()
|
||||
}
|
||||
var result []*ast.Node
|
||||
for _, unionType := range types {
|
||||
if prop := c.GetPropertyOfType(unionType, name); prop != nil {
|
||||
result = append(result, prop.Declarations...)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node = getDeclarationNameForKeyword(node)
|
||||
if symbol := c.GetSymbolAtLocation(node); symbol != nil {
|
||||
if symbol.Flags&ast.SymbolFlagsClass != 0 && symbol.Flags&(ast.SymbolFlagsFunction|ast.SymbolFlagsVariable) == 0 && node.Kind == ast.KindConstructorKeyword {
|
||||
if constructor := symbol.Members[ast.InternalSymbolNameConstructor]; constructor != nil {
|
||||
symbol = constructor
|
||||
}
|
||||
}
|
||||
if symbol.Flags&ast.SymbolFlagsAlias != 0 {
|
||||
if resolved, ok := c.ResolveAlias(symbol); ok {
|
||||
symbol = resolved
|
||||
}
|
||||
}
|
||||
objectLiteralElementDeclarations := getDeclarationsFromObjectLiteralElement(c, node)
|
||||
if len(objectLiteralElementDeclarations) > 0 {
|
||||
return objectLiteralElementDeclarations
|
||||
}
|
||||
if len(symbol.Declarations) > 0 {
|
||||
return symbol.Declarations
|
||||
}
|
||||
}
|
||||
if indexInfos := c.GetIndexSignaturesAtLocation(node); len(indexInfos) != 0 {
|
||||
return indexInfos
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDeclarationsFromObjectLiteralElement returns declarations from the contextual type
|
||||
// of an object literal element, if available.
|
||||
func getDeclarationsFromObjectLiteralElement(c *checker.Checker, node *ast.Node) []*ast.Node {
|
||||
element := getContainingObjectLiteralElement(node)
|
||||
if element == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
contextualType := c.GetContextualType(element.Parent, checker.ContextFlagsNone)
|
||||
if contextualType == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
properties := c.GetPropertySymbolsFromContextualType(element, contextualType, false /*unionSymbolOk*/)
|
||||
if core.Some(properties, func(p *ast.Symbol) bool {
|
||||
return p.ValueDeclaration != nil && ast.IsObjectLiteralExpression(p.ValueDeclaration.Parent) && ast.IsObjectLiteralElement(p.ValueDeclaration) && p.ValueDeclaration.Name() == node
|
||||
}) {
|
||||
if withoutNodeInferencesType := c.GetContextualType(element.Parent, checker.ContextFlagsIgnoreNodeInferences); withoutNodeInferencesType != nil {
|
||||
if withoutNodeInferencesProperties := c.GetPropertySymbolsFromContextualType(element, withoutNodeInferencesType, false /*unionSymbolOk*/); len(withoutNodeInferencesProperties) > 0 {
|
||||
properties = withoutNodeInferencesProperties
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result []*ast.Node
|
||||
for _, prop := range properties {
|
||||
result = append(result, prop.Declarations...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Returns a CallLikeExpression where `node` is the target being invoked.
|
||||
func getAncestorCallLikeExpression(node *ast.Node) *ast.Node {
|
||||
target := ast.FindAncestor(node, func(n *ast.Node) bool {
|
||||
return !ast.IsRightSideOfPropertyAccess(n)
|
||||
})
|
||||
callLike := target.Parent
|
||||
if callLike != nil && ast.IsCallLikeExpression(callLike) && ast.GetInvokedExpression(callLike) == target {
|
||||
return callLike
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tryGetSignatureDeclaration(typeChecker *checker.Checker, node *ast.Node) *ast.Node {
|
||||
var signature *checker.Signature
|
||||
callLike := getAncestorCallLikeExpression(node)
|
||||
if callLike != nil {
|
||||
signature = typeChecker.GetResolvedSignature(callLike)
|
||||
}
|
||||
// Don't go to a function type, go to the value having that type.
|
||||
var declaration *ast.Node
|
||||
if signature != nil && signature.Declaration() != nil {
|
||||
declaration = signature.Declaration()
|
||||
if ast.IsFunctionLike(declaration) && !ast.IsFunctionTypeNode(declaration) {
|
||||
return declaration
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isJsxConstructorLike(node *ast.Node) bool {
|
||||
switch {
|
||||
case ast.IsConstructorDeclaration(node),
|
||||
ast.IsConstructorTypeNode(node),
|
||||
ast.IsCallSignatureDeclaration(node),
|
||||
ast.IsConstructSignatureDeclaration(node):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func symbolMatchesSignature(symbol *ast.Symbol, calledDeclaration *ast.Node) bool {
|
||||
if symbol == nil || calledDeclaration == nil {
|
||||
return false
|
||||
}
|
||||
calledSymbol := calledDeclaration.Symbol()
|
||||
if symbol == calledSymbol || calledSymbol != nil && symbol == calledSymbol.Parent {
|
||||
return true
|
||||
}
|
||||
parent := calledDeclaration.Parent
|
||||
return parent != nil && (ast.IsAssignmentExpression(parent, false /*excludeCompoundAssignment*/) ||
|
||||
!ast.IsCallLikeExpression(parent) && ast.CanHaveSymbol(parent) && symbol == parent.Symbol())
|
||||
}
|
||||
|
||||
func getSymbolForOverriddenMember(typeChecker *checker.Checker, node *ast.Node) *ast.Symbol {
|
||||
classElement := ast.FindAncestor(node, ast.IsClassElement)
|
||||
if classElement == nil || classElement.Name() == nil {
|
||||
return nil
|
||||
}
|
||||
baseDeclaration := ast.FindAncestor(classElement, ast.IsClassLike)
|
||||
if baseDeclaration == nil {
|
||||
return nil
|
||||
}
|
||||
baseTypeNode := ast.GetClassExtendsHeritageElement(baseDeclaration)
|
||||
if baseTypeNode == nil {
|
||||
return nil
|
||||
}
|
||||
expression := ast.SkipParentheses(baseTypeNode.Expression())
|
||||
var base *ast.Symbol
|
||||
if ast.IsClassExpression(expression) {
|
||||
base = expression.Symbol()
|
||||
} else {
|
||||
base = typeChecker.GetSymbolAtLocation(expression)
|
||||
}
|
||||
if base == nil {
|
||||
return nil
|
||||
}
|
||||
name := ast.GetTextOfPropertyName(classElement.Name())
|
||||
if ast.HasStaticModifier(classElement) {
|
||||
return typeChecker.GetPropertyOfType(typeChecker.GetTypeOfSymbol(base), name)
|
||||
}
|
||||
return typeChecker.GetPropertyOfType(typeChecker.GetDeclaredTypeOfSymbol(base), name)
|
||||
}
|
||||
|
||||
func getTypeOfSymbolAtLocation(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) *checker.Type {
|
||||
t := c.GetTypeOfSymbolAtLocation(symbol, node)
|
||||
// If the type is just a function's inferred type, go-to-type should go to the return type instead since
|
||||
// go-to-definition takes you to the function anyway.
|
||||
if t.Symbol() == symbol || t.Symbol() != nil && symbol.ValueDeclaration != nil && ast.IsVariableDeclaration(symbol.ValueDeclaration) && symbol.ValueDeclaration.Initializer() == t.Symbol().ValueDeclaration {
|
||||
sigs := c.GetCallSignatures(t)
|
||||
if len(sigs) == 1 {
|
||||
return c.GetReturnTypeOfSignature(sigs[0])
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func getDeclarationsFromType(t *checker.Type) []*ast.Node {
|
||||
var result []*ast.Node
|
||||
for _, t := range t.Distributed() {
|
||||
if t.Symbol() != nil {
|
||||
for _, decl := range t.Symbol().Declarations {
|
||||
result = core.AppendIfUnique(result, decl)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
58
tools/tsgo/internal/ls/diagnostics.go
Normal file
58
tools/tsgo/internal/ls/diagnostics.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
)
|
||||
|
||||
// getAllDiagnostics collects all diagnostics for a file: syntactic, semantic,
|
||||
// suggestion, and (when declarations are emitted) declaration diagnostics.
|
||||
func getAllDiagnostics(ctx context.Context, program *compiler.Program, file *ast.SourceFile) []*ast.Diagnostic {
|
||||
var diags []*ast.Diagnostic
|
||||
diags = append(diags, program.GetSyntacticDiagnostics(ctx, file)...)
|
||||
diags = append(diags, program.GetSemanticDiagnostics(ctx, file)...)
|
||||
diags = append(diags, program.GetSuggestionDiagnostics(ctx, file)...)
|
||||
if program.Options().GetEmitDeclarations() {
|
||||
diags = append(diags, program.GetDeclarationDiagnostics(ctx, file)...)
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
func (l *LanguageService) ProvideDiagnostics(ctx context.Context, uri lsproto.DocumentUri) (lsproto.DocumentDiagnosticResponse, error) {
|
||||
program, file := l.getProgramAndFile(uri)
|
||||
|
||||
if l.UserPreferences().EnableValidation.IsFalse() {
|
||||
diagnostics := []*lsproto.Diagnostic{}
|
||||
return lsproto.RelatedFullDocumentDiagnosticReportOrUnchangedDocumentDiagnosticReport{
|
||||
FullDocumentDiagnosticReport: &lsproto.RelatedFullDocumentDiagnosticReport{
|
||||
Items: diagnostics,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
diagnostics := getAllDiagnostics(ctx, program, file)
|
||||
|
||||
return lsproto.RelatedFullDocumentDiagnosticReportOrUnchangedDocumentDiagnosticReport{
|
||||
FullDocumentDiagnosticReport: &lsproto.RelatedFullDocumentDiagnosticReport{
|
||||
Items: l.toLSPDiagnostics(ctx, diagnostics),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) toLSPDiagnostics(ctx context.Context, diagnostics ...[]*ast.Diagnostic) []*lsproto.Diagnostic {
|
||||
size := 0
|
||||
for _, diagSlice := range diagnostics {
|
||||
size += len(diagSlice)
|
||||
}
|
||||
lspDiagnostics := make([]*lsproto.Diagnostic, 0, size)
|
||||
for _, diagSlice := range diagnostics {
|
||||
for _, diag := range diagSlice {
|
||||
lspDiagnostics = append(lspDiagnostics, lsconv.DiagnosticToLSPPull(ctx, l.converters, diag, l.UserPreferences().ReportStyleChecksAsWarnings.IsTrue()))
|
||||
}
|
||||
}
|
||||
return lspDiagnostics
|
||||
}
|
||||
217
tools/tsgo/internal/ls/displaypartswriter.go
Normal file
217
tools/tsgo/internal/ls/displaypartswriter.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
var _ printer.EmitTextWriter = &displayPartsWriter{}
|
||||
|
||||
// displayPartsWriter implements EmitTextWriter and captures classified text runs
|
||||
// for VS colorized labels, while also building a plain string.
|
||||
// When vsCapability is false, only the plain string is built; runs are skipped.
|
||||
type displayPartsWriter struct {
|
||||
builder strings.Builder
|
||||
runs []*lsproto.VSClassifiedTextRun
|
||||
vsCapability bool
|
||||
lastWritten string
|
||||
}
|
||||
|
||||
func newDisplayPartsWriter(vsCapability bool) *displayPartsWriter {
|
||||
return &displayPartsWriter{vsCapability: vsCapability}
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) addRun(classification lsproto.ClassificationTypeName, text string) {
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
if w.vsCapability {
|
||||
w.runs = append(w.runs, &lsproto.VSClassifiedTextRun{
|
||||
ClassificationTypeName: string(classification),
|
||||
Text: text,
|
||||
})
|
||||
}
|
||||
w.lastWritten = text
|
||||
w.builder.WriteString(text)
|
||||
}
|
||||
|
||||
// WriteClassified writes text with an explicit classification type.
|
||||
func (w *displayPartsWriter) WriteClassified(text string, classification lsproto.ClassificationTypeName) {
|
||||
w.addRun(classification, text)
|
||||
}
|
||||
|
||||
// WriteFrom copies the accumulated content from another displayPartsWriter.
|
||||
func (w *displayPartsWriter) WriteFrom(other *displayPartsWriter) {
|
||||
w.builder.WriteString(other.String())
|
||||
if w.vsCapability {
|
||||
w.runs = append(w.runs, other.GetRuns()...)
|
||||
}
|
||||
if other.lastWritten != "" {
|
||||
w.lastWritten = other.lastWritten
|
||||
}
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) GetRuns() []*lsproto.VSClassifiedTextRun {
|
||||
return w.runs
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) String() string {
|
||||
return w.builder.String()
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) Clear() {
|
||||
w.lastWritten = ""
|
||||
w.builder.Reset()
|
||||
w.runs = nil
|
||||
}
|
||||
|
||||
func (w displayPartsWriter) DecreaseIndent() {}
|
||||
|
||||
func (w displayPartsWriter) GetColumn() core.UTF16Offset { return 0 }
|
||||
|
||||
func (w displayPartsWriter) GetIndent() int { return 0 }
|
||||
|
||||
func (w displayPartsWriter) GetLine() int { return 0 }
|
||||
|
||||
func (w displayPartsWriter) GetTextPos() int {
|
||||
return w.builder.Len()
|
||||
}
|
||||
|
||||
func (w displayPartsWriter) HasTrailingComment() bool { return false }
|
||||
|
||||
func (w displayPartsWriter) HasTrailingWhitespace() bool {
|
||||
if w.builder.Len() == 0 {
|
||||
return false
|
||||
}
|
||||
ch, _ := utf8.DecodeLastRuneInString(w.lastWritten)
|
||||
if ch == utf8.RuneError {
|
||||
return false
|
||||
}
|
||||
return stringutil.IsWhiteSpaceLike(ch)
|
||||
}
|
||||
|
||||
func (w displayPartsWriter) IncreaseIndent() {}
|
||||
|
||||
func (w displayPartsWriter) IsAtStartOfLine() bool { return false }
|
||||
|
||||
func (w *displayPartsWriter) RawWrite(s string) {
|
||||
w.addRun(lsproto.ClassificationTypeNameText, s)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) Write(s string) {
|
||||
w.addRun(lsproto.ClassificationTypeNameText, s)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteComment(text string) {
|
||||
// Strada's writeComment uses unknownWrite → SymbolDisplayPartKind.text → "text"
|
||||
w.addRun(lsproto.ClassificationTypeNameText, text)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteKeyword(text string) {
|
||||
w.addRun(lsproto.ClassificationTypeNameKeyword, text)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteLine() {
|
||||
w.addRun(lsproto.ClassificationTypeNameWhiteSpace, " ")
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteLineForce(force bool) {
|
||||
w.addRun(lsproto.ClassificationTypeNameWhiteSpace, " ")
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteLiteral(s string) {
|
||||
// Strada's writeLiteral → SymbolDisplayPartKind.stringLiteral → "string"
|
||||
w.addRun(lsproto.ClassificationTypeNameString, s)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteOperator(text string) {
|
||||
w.addRun(lsproto.ClassificationTypeNameOperator, text)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteParameter(text string) {
|
||||
w.addRun(lsproto.ClassificationTypeNameParameterName, text)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteProperty(text string) {
|
||||
w.addRun(lsproto.ClassificationTypeNamePropertyName, text)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WritePunctuation(text string) {
|
||||
w.addRun(lsproto.ClassificationTypeNamePunctuation, text)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteSpace(text string) {
|
||||
w.addRun(lsproto.ClassificationTypeNameWhiteSpace, text)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteStringLiteral(text string) {
|
||||
w.addRun(lsproto.ClassificationTypeNameString, text)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteSymbol(text string, symbol *ast.Symbol) {
|
||||
classification := classificationForSymbol(symbol)
|
||||
w.addRun(classification, text)
|
||||
}
|
||||
|
||||
func (w *displayPartsWriter) WriteTrailingSemicolon(text string) {
|
||||
w.addRun(lsproto.ClassificationTypeNamePunctuation, text)
|
||||
}
|
||||
|
||||
// classificationForSymbol determines the Roslyn classification type name based on a symbol's flags.
|
||||
// Matches the Strada translation chain: displayPartKind() → GetClassificationName().
|
||||
func classificationForSymbol(symbol *ast.Symbol) lsproto.ClassificationTypeName {
|
||||
if symbol == nil {
|
||||
return lsproto.ClassificationTypeNameText
|
||||
}
|
||||
flags := symbol.Flags
|
||||
switch {
|
||||
case flags&ast.SymbolFlagsVariable != 0:
|
||||
if isFirstDeclarationOfSymbolParameter(symbol) {
|
||||
return lsproto.ClassificationTypeNameParameterName
|
||||
}
|
||||
return lsproto.ClassificationTypeNameLocalName
|
||||
case flags&ast.SymbolFlagsProperty != 0:
|
||||
return lsproto.ClassificationTypeNamePropertyName
|
||||
case flags&ast.SymbolFlagsGetAccessor != 0:
|
||||
return lsproto.ClassificationTypeNamePropertyName
|
||||
case flags&ast.SymbolFlagsSetAccessor != 0:
|
||||
return lsproto.ClassificationTypeNamePropertyName
|
||||
case flags&ast.SymbolFlagsEnumMember != 0:
|
||||
return lsproto.ClassificationTypeNameFieldName
|
||||
case flags&ast.SymbolFlagsFunction != 0:
|
||||
return lsproto.ClassificationTypeNameMethodName
|
||||
case flags&ast.SymbolFlagsClass != 0:
|
||||
return lsproto.ClassificationTypeNameClassName
|
||||
case flags&ast.SymbolFlagsInterface != 0:
|
||||
return lsproto.ClassificationTypeNameInterfaceName
|
||||
case flags&ast.SymbolFlagsEnum != 0:
|
||||
return lsproto.ClassificationTypeNameEnumName
|
||||
case flags&ast.SymbolFlagsModule != 0:
|
||||
return lsproto.ClassificationTypeNameModuleName
|
||||
case flags&ast.SymbolFlagsMethod != 0:
|
||||
return lsproto.ClassificationTypeNameMethodName
|
||||
case flags&ast.SymbolFlagsTypeParameter != 0:
|
||||
return lsproto.ClassificationTypeNameTypeParameterName
|
||||
case flags&ast.SymbolFlagsTypeAlias != 0:
|
||||
return lsproto.ClassificationTypeNameIdentifier
|
||||
case flags&ast.SymbolFlagsAlias != 0:
|
||||
return lsproto.ClassificationTypeNameIdentifier
|
||||
default:
|
||||
return lsproto.ClassificationTypeNameText
|
||||
}
|
||||
}
|
||||
|
||||
// isFirstDeclarationOfSymbolParameter checks if the symbol's first declaration is a parameter.
|
||||
func isFirstDeclarationOfSymbolParameter(symbol *ast.Symbol) bool {
|
||||
declarations := symbol.Declarations
|
||||
if len(declarations) == 0 {
|
||||
return false
|
||||
}
|
||||
return declarations[0].Kind == ast.KindParameter
|
||||
}
|
||||
752
tools/tsgo/internal/ls/documenthighlights.go
Normal file
752
tools/tsgo/internal/ls/documenthighlights.go
Normal file
@@ -0,0 +1,752 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
)
|
||||
|
||||
func (l *LanguageService) ProvideDocumentHighlights(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position) (lsproto.DocumentHighlightResponse, error) {
|
||||
result, err := l.provideDocumentHighlightsWorker(ctx, documentUri, documentPosition, nil)
|
||||
if err != nil {
|
||||
return lsproto.DocumentHighlightsOrNull{}, err
|
||||
}
|
||||
// Extract highlights for the current file only.
|
||||
var documentHighlights []*lsproto.DocumentHighlight
|
||||
if result.MultiDocumentHighlights != nil {
|
||||
for _, mh := range *result.MultiDocumentHighlights {
|
||||
if mh.Uri == documentUri {
|
||||
documentHighlights = append(documentHighlights, mh.Highlights...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return lsproto.DocumentHighlightsOrNull{DocumentHighlights: &documentHighlights}, nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) ProvideMultiDocumentHighlights(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position, filesToSearch []lsproto.DocumentUri) (lsproto.CustomMultiDocumentHighlightResponse, error) {
|
||||
return l.provideDocumentHighlightsWorker(ctx, documentUri, documentPosition, filesToSearch)
|
||||
}
|
||||
|
||||
func (l *LanguageService) provideDocumentHighlightsWorker(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position, filesToSearch []lsproto.DocumentUri) (lsproto.MultiDocumentHighlightsOrNull, error) {
|
||||
program, sourceFile := l.getProgramAndFile(documentUri)
|
||||
position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition))
|
||||
node := astnav.GetTouchingPropertyName(sourceFile, position)
|
||||
|
||||
// Cheap JSX check before resolving files to search.
|
||||
if node.Parent != nil && (node.Parent.Kind == ast.KindJsxClosingElement || (node.Parent.Kind == ast.KindJsxOpeningElement && node.Parent.TagName() == node)) {
|
||||
var openingElement, closingElement *ast.Node
|
||||
if ast.IsJsxElement(node.Parent.Parent) {
|
||||
openingElement = node.Parent.Parent.AsJsxElement().OpeningElement
|
||||
closingElement = node.Parent.Parent.AsJsxElement().ClosingElement
|
||||
}
|
||||
var highlights []*lsproto.DocumentHighlight
|
||||
kind := lsproto.DocumentHighlightKindRead
|
||||
if openingElement != nil {
|
||||
highlights = append(highlights, &lsproto.DocumentHighlight{
|
||||
Range: l.createLspRangeFromNode(openingElement, sourceFile),
|
||||
Kind: &kind,
|
||||
})
|
||||
}
|
||||
if closingElement != nil {
|
||||
highlights = append(highlights, &lsproto.DocumentHighlight{
|
||||
Range: l.createLspRangeFromNode(closingElement, sourceFile),
|
||||
Kind: &kind,
|
||||
})
|
||||
}
|
||||
multiHighlights := []*lsproto.MultiDocumentHighlight{
|
||||
{Uri: documentUri, Highlights: highlights},
|
||||
}
|
||||
return lsproto.MultiDocumentHighlightsOrNull{
|
||||
MultiDocumentHighlights: &multiHighlights,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Resolve the source files to search, deduplicating by file name.
|
||||
var sourceFiles []*ast.SourceFile
|
||||
seenFiles := collections.NewSetWithSizeHint[string](len(filesToSearch))
|
||||
for _, uri := range filesToSearch {
|
||||
fileName := uri.FileName()
|
||||
if !seenFiles.AddIfAbsent(fileName) {
|
||||
continue
|
||||
}
|
||||
if sf := program.GetSourceFile(fileName); sf != nil {
|
||||
sourceFiles = append(sourceFiles, sf)
|
||||
}
|
||||
}
|
||||
if len(sourceFiles) == 0 {
|
||||
sourceFiles = []*ast.SourceFile{sourceFile}
|
||||
}
|
||||
|
||||
multiHighlights := l.getSemanticDocumentHighlights(ctx, position, node, program, sourceFiles)
|
||||
if len(multiHighlights) == 0 {
|
||||
// Fall back to syntactic highlights for the current file only.
|
||||
syntacticHighlights := l.getSyntacticDocumentHighlights(node, sourceFile)
|
||||
if len(syntacticHighlights) > 0 {
|
||||
multiHighlights = []*lsproto.MultiDocumentHighlight{
|
||||
{Uri: documentUri, Highlights: syntacticHighlights},
|
||||
}
|
||||
}
|
||||
}
|
||||
return lsproto.MultiDocumentHighlightsOrNull{MultiDocumentHighlights: &multiHighlights}, nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFiles []*ast.SourceFile) []*lsproto.MultiDocumentHighlight {
|
||||
options := refOptions{use: referenceUseNone}
|
||||
referenceEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, sourceFiles, options)
|
||||
if referenceEntries == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Group highlights by file
|
||||
fileHighlights := make(map[string][]*lsproto.DocumentHighlight)
|
||||
for _, entry := range referenceEntries {
|
||||
for _, ref := range entry.references {
|
||||
fileName, highlight := l.toDocumentHighlight(ref)
|
||||
fileHighlights[fileName] = append(fileHighlights[fileName], highlight)
|
||||
}
|
||||
}
|
||||
|
||||
var result []*lsproto.MultiDocumentHighlight
|
||||
for _, sf := range sourceFiles {
|
||||
if highlights, ok := fileHighlights[sf.FileName()]; ok {
|
||||
result = append(result, &lsproto.MultiDocumentHighlight{
|
||||
Uri: lsconv.FileNameToDocumentURI(sf.FileName()),
|
||||
Highlights: highlights,
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (l *LanguageService) toDocumentHighlight(entry *ReferenceEntry) (string, *lsproto.DocumentHighlight) {
|
||||
entry = l.resolveEntry(entry)
|
||||
|
||||
kind := lsproto.DocumentHighlightKindRead
|
||||
if entry.kind == entryKindRange {
|
||||
return entry.fileName, &lsproto.DocumentHighlight{
|
||||
Range: l.getRangeOfEntry(entry),
|
||||
Kind: &kind,
|
||||
}
|
||||
}
|
||||
|
||||
// Determine write access for node references.
|
||||
if ast.IsWriteAccessForReference(entry.node) {
|
||||
kind = lsproto.DocumentHighlightKindWrite
|
||||
}
|
||||
|
||||
dh := &lsproto.DocumentHighlight{
|
||||
Range: l.getRangeOfEntry(entry),
|
||||
Kind: &kind,
|
||||
}
|
||||
|
||||
return entry.fileName, dh
|
||||
}
|
||||
|
||||
func (l *LanguageService) getSyntacticDocumentHighlights(node *ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight {
|
||||
switch node.Kind {
|
||||
case ast.KindIfKeyword, ast.KindElseKeyword:
|
||||
if ast.IsIfStatement(node.Parent) {
|
||||
return l.getIfElseOccurrences(node.Parent.AsIfStatement(), sourceFile)
|
||||
}
|
||||
return nil
|
||||
case ast.KindReturnKeyword:
|
||||
return l.useParent(node.Parent, ast.IsReturnStatement, getReturnOccurrences, sourceFile)
|
||||
case ast.KindThrowKeyword:
|
||||
return l.useParent(node.Parent, ast.IsThrowStatement, getThrowOccurrences, sourceFile)
|
||||
case ast.KindTryKeyword, ast.KindCatchKeyword, ast.KindFinallyKeyword:
|
||||
var tryStatement *ast.Node
|
||||
if node.Kind == ast.KindCatchKeyword {
|
||||
tryStatement = node.Parent.Parent
|
||||
} else {
|
||||
tryStatement = node.Parent
|
||||
}
|
||||
return l.useParent(tryStatement, ast.IsTryStatement, getTryCatchFinallyOccurrences, sourceFile)
|
||||
case ast.KindSwitchKeyword:
|
||||
return l.useParent(node.Parent, ast.IsSwitchStatement, getSwitchCaseDefaultOccurrences, sourceFile)
|
||||
case ast.KindCaseKeyword, ast.KindDefaultKeyword:
|
||||
if ast.IsDefaultClause(node.Parent) || ast.IsCaseClause(node.Parent) {
|
||||
return l.useParent(node.Parent.Parent.Parent, ast.IsSwitchStatement, getSwitchCaseDefaultOccurrences, sourceFile)
|
||||
}
|
||||
return nil
|
||||
case ast.KindBreakKeyword, ast.KindContinueKeyword:
|
||||
return l.useParent(node.Parent, ast.IsBreakOrContinueStatement, getBreakOrContinueStatementOccurrences, sourceFile)
|
||||
case ast.KindForKeyword, ast.KindWhileKeyword, ast.KindDoKeyword:
|
||||
return l.useParent(node.Parent, func(n *ast.Node) bool {
|
||||
return ast.IsIterationStatement(n, true)
|
||||
}, getLoopBreakContinueOccurrences, sourceFile)
|
||||
case ast.KindConstructorKeyword:
|
||||
return l.getFromAllDeclarations(ast.IsConstructorDeclaration, []ast.Kind{ast.KindConstructorKeyword}, node, sourceFile)
|
||||
case ast.KindGetKeyword, ast.KindSetKeyword:
|
||||
return l.getFromAllDeclarations(ast.IsAccessor, []ast.Kind{ast.KindGetKeyword, ast.KindSetKeyword}, node, sourceFile)
|
||||
case ast.KindAwaitKeyword:
|
||||
return l.useParent(node.Parent, ast.IsAwaitExpression, getAsyncAndAwaitOccurrences, sourceFile)
|
||||
case ast.KindAsyncKeyword:
|
||||
return l.highlightSpans(getAsyncAndAwaitOccurrences(node, sourceFile), sourceFile)
|
||||
case ast.KindYieldKeyword:
|
||||
return l.highlightSpans(getYieldOccurrences(node, sourceFile), sourceFile)
|
||||
case ast.KindInKeyword, ast.KindOutKeyword:
|
||||
return nil
|
||||
default:
|
||||
if ast.IsModifierKind(node.Kind) && (ast.IsDeclaration(node.Parent) || ast.IsVariableStatement(node.Parent)) {
|
||||
return l.highlightSpans(getModifierOccurrences(node.Kind, node.Parent, sourceFile), sourceFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LanguageService) useParent(node *ast.Node, nodeTest func(*ast.Node) bool, getNodes func(*ast.Node, *ast.SourceFile) []*ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight {
|
||||
if nodeTest(node) {
|
||||
return l.highlightSpans(getNodes(node, sourceFile), sourceFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) highlightSpans(nodes []*ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight {
|
||||
if len(nodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
var highlights []*lsproto.DocumentHighlight
|
||||
kind := lsproto.DocumentHighlightKindRead
|
||||
for _, node := range nodes {
|
||||
if node != nil {
|
||||
highlights = append(highlights, &lsproto.DocumentHighlight{
|
||||
Range: l.createLspRangeFromNode(node, sourceFile),
|
||||
Kind: &kind,
|
||||
})
|
||||
}
|
||||
}
|
||||
return highlights
|
||||
}
|
||||
|
||||
func (l *LanguageService) getFromAllDeclarations(nodeTest func(*ast.Node) bool, keywords []ast.Kind, node *ast.Node, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight {
|
||||
return l.useParent(node.Parent, nodeTest, func(decl *ast.Node, sf *ast.SourceFile) []*ast.Node {
|
||||
var symbolDecls []*ast.Node
|
||||
if ast.CanHaveSymbol(decl) {
|
||||
if symbol := decl.Symbol(); symbol != nil {
|
||||
for _, d := range symbol.Declarations {
|
||||
if nodeTest(d) {
|
||||
outer:
|
||||
for _, c := range getChildrenFromNonJSDocNode(d, sourceFile) {
|
||||
for _, k := range keywords {
|
||||
if c.Kind == k {
|
||||
symbolDecls = append(symbolDecls, c)
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return symbolDecls
|
||||
}, sourceFile)
|
||||
}
|
||||
|
||||
func (l *LanguageService) getIfElseOccurrences(ifStatement *ast.IfStatement, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight {
|
||||
keywords := getIfElseKeywords(ifStatement, sourceFile)
|
||||
kind := lsproto.DocumentHighlightKindRead
|
||||
var highlights []*lsproto.DocumentHighlight
|
||||
|
||||
// We'd like to highlight else/ifs together if they are only separated by whitespace
|
||||
// (i.e. the keywords are separated by no comments, no newlines).
|
||||
for i := 0; i < len(keywords); i++ {
|
||||
if keywords[i].Kind == ast.KindElseKeyword && i < len(keywords)-1 {
|
||||
elseKeyword := keywords[i]
|
||||
ifKeyword := keywords[i+1] // this *should* always be an 'if' keyword.
|
||||
shouldCombine := true
|
||||
|
||||
// Avoid recalculating getStart() by iterating backwards.
|
||||
ifTokenStart := scanner.GetTokenPosOfNode(ifKeyword, sourceFile, false)
|
||||
if ifTokenStart < 0 {
|
||||
ifTokenStart = ifKeyword.Pos()
|
||||
}
|
||||
for j := ifTokenStart - 1; j >= elseKeyword.End(); j-- {
|
||||
if !stringutil.IsWhiteSpaceSingleLine(rune(sourceFile.Text()[j])) {
|
||||
shouldCombine = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if shouldCombine {
|
||||
highlights = append(highlights, &lsproto.DocumentHighlight{
|
||||
Range: l.createLspRangeFromBounds(scanner.SkipTrivia(sourceFile.Text(), elseKeyword.Pos()), ifKeyword.End(), sourceFile),
|
||||
Kind: &kind,
|
||||
})
|
||||
i++ // skip the next keyword
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Ordinary case: just highlight the keyword.
|
||||
highlights = append(highlights, &lsproto.DocumentHighlight{
|
||||
Range: l.createLspRangeFromNode(keywords[i], sourceFile),
|
||||
Kind: &kind,
|
||||
})
|
||||
}
|
||||
return highlights
|
||||
}
|
||||
|
||||
func getIfElseKeywords(ifStatement *ast.IfStatement, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
// We may be at an if statement like those in the range below:
|
||||
//
|
||||
// ```
|
||||
// if (...) {
|
||||
// } else [|if (...) {}|]
|
||||
// ````
|
||||
//
|
||||
// Traverse upwards through all parent if-statements linked by their else-branches.
|
||||
for ast.IsIfStatement(ifStatement.Parent) {
|
||||
// See if the parent's `else` is actually the current `if` statement.
|
||||
parentingIf := ifStatement.Parent.AsIfStatement()
|
||||
elseStatement := parentingIf.ElseStatement
|
||||
if elseStatement != ifStatement.AsNode() {
|
||||
break
|
||||
}
|
||||
ifStatement = parentingIf
|
||||
}
|
||||
|
||||
var keywords []*ast.Node
|
||||
|
||||
// Traverse back down through the else branches, aggregating if/else keywords of if-statements.
|
||||
for {
|
||||
children := getChildrenFromNonJSDocNode(ifStatement.AsNode(), sourceFile)
|
||||
if len(children) > 0 && children[0].Kind == ast.KindIfKeyword {
|
||||
keywords = append(keywords, children[0])
|
||||
}
|
||||
// Generally the 'else' keyword is second-to-last, so traverse backwards.
|
||||
for i := len(children) - 1; i >= 0; i-- {
|
||||
if children[i].Kind == ast.KindElseKeyword {
|
||||
keywords = append(keywords, children[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
elseStatement := ifStatement.ElseStatement
|
||||
if elseStatement == nil || !ast.IsIfStatement(elseStatement) {
|
||||
break
|
||||
}
|
||||
ifStatement = elseStatement.AsIfStatement()
|
||||
}
|
||||
return keywords
|
||||
}
|
||||
|
||||
func getReturnOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
funcNode := ast.FindAncestor(node.Parent, ast.IsFunctionLike)
|
||||
if funcNode == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var keywords []*ast.Node
|
||||
body := funcNode.Body()
|
||||
if body != nil {
|
||||
ast.ForEachReturnStatement(body, func(ret *ast.Node) bool {
|
||||
keyword := astnav.FindChildOfKind(ret, ast.KindReturnKeyword, sourceFile)
|
||||
if keyword != nil {
|
||||
keywords = append(keywords, keyword)
|
||||
}
|
||||
return false // continue traversal
|
||||
})
|
||||
|
||||
// Get all throw statements not in a try block
|
||||
throwStatements := aggregateOwnedThrowStatements(body, sourceFile)
|
||||
for _, throw := range throwStatements {
|
||||
keyword := astnav.FindChildOfKind(throw, ast.KindThrowKeyword, sourceFile)
|
||||
if keyword != nil {
|
||||
keywords = append(keywords, keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
return keywords
|
||||
}
|
||||
|
||||
func aggregateOwnedThrowStatements(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
if ast.IsThrowStatement(node) {
|
||||
return []*ast.Node{node}
|
||||
}
|
||||
if ast.IsTryStatement(node) {
|
||||
// Exceptions thrown within a try block lacking a catch clause are "owned" in the current context.
|
||||
statement := node.AsTryStatement()
|
||||
tryBlock := statement.TryBlock
|
||||
catchClause := statement.CatchClause
|
||||
finallyBlock := statement.FinallyBlock
|
||||
|
||||
var result []*ast.Node
|
||||
if catchClause != nil {
|
||||
result = aggregateOwnedThrowStatements(catchClause, sourceFile)
|
||||
} else if tryBlock != nil {
|
||||
result = aggregateOwnedThrowStatements(tryBlock, sourceFile)
|
||||
}
|
||||
if finallyBlock != nil {
|
||||
result = append(result, aggregateOwnedThrowStatements(finallyBlock, sourceFile)...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
// Do not cross function boundaries.
|
||||
if ast.IsFunctionLike(node) {
|
||||
return nil
|
||||
}
|
||||
return flatMapChildren(node, sourceFile, aggregateOwnedThrowStatements)
|
||||
}
|
||||
|
||||
func flatMapChildren[T any](node *ast.Node, sourceFile *ast.SourceFile, cb func(child *ast.Node, sourceFile *ast.SourceFile) []T) []T {
|
||||
var result []T
|
||||
|
||||
node.ForEachChild(func(child *ast.Node) bool {
|
||||
value := cb(child, sourceFile)
|
||||
if value != nil {
|
||||
result = append(result, value...)
|
||||
}
|
||||
return false // continue traversal
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func getThrowOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
owner := getThrowStatementOwner(node)
|
||||
if owner == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var keywords []*ast.Node
|
||||
|
||||
// Aggregate all throw statements "owned" by this owner.
|
||||
throwStatements := aggregateOwnedThrowStatements(owner, sourceFile)
|
||||
for _, throw := range throwStatements {
|
||||
keyword := astnav.FindChildOfKind(throw, ast.KindThrowKeyword, sourceFile)
|
||||
if keyword != nil {
|
||||
keywords = append(keywords, keyword)
|
||||
}
|
||||
}
|
||||
|
||||
// If the "owner" is a function, then we equate 'return' and 'throw' statements in their
|
||||
// ability to "jump out" of the function, and include occurrences for both
|
||||
if ast.IsFunctionBlock(owner) {
|
||||
ast.ForEachReturnStatement(owner, func(ret *ast.Node) bool {
|
||||
keyword := astnav.FindChildOfKind(ret, ast.KindReturnKeyword, sourceFile)
|
||||
if keyword != nil {
|
||||
keywords = append(keywords, keyword)
|
||||
}
|
||||
return false // continue traversal
|
||||
})
|
||||
}
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
// For lack of a better name, this function takes a throw statement and returns the
|
||||
// nearest ancestor that is a try-block (whose try statement has a catch clause),
|
||||
// function-block, or source file.
|
||||
func getThrowStatementOwner(throwStatement *ast.Node) *ast.Node {
|
||||
child := throwStatement
|
||||
for child.Parent != nil {
|
||||
parent := child.Parent
|
||||
|
||||
if ast.IsFunctionBlock(parent) || parent.Kind == ast.KindSourceFile {
|
||||
return parent
|
||||
}
|
||||
|
||||
// A throw-statement is only owned by a try-statement if the try-statement has
|
||||
// a catch clause, and if the throw-statement occurs within the try block.
|
||||
if ast.IsTryStatement(parent) {
|
||||
tryStatement := parent.AsTryStatement()
|
||||
if tryStatement.TryBlock == child && tryStatement.CatchClause != nil {
|
||||
return child
|
||||
}
|
||||
}
|
||||
|
||||
child = parent
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTryCatchFinallyOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
tryStatement := node.AsTryStatement()
|
||||
|
||||
var keywords []*ast.Node
|
||||
token := lsutil.GetFirstToken(node, sourceFile)
|
||||
if token != nil && token.Kind == ast.KindTryKeyword {
|
||||
keywords = append(keywords, token)
|
||||
}
|
||||
|
||||
if tryStatement.CatchClause != nil {
|
||||
if catchToken := astnav.FindChildOfKind(node, ast.KindCatchKeyword, sourceFile); catchToken != nil {
|
||||
keywords = append(keywords, catchToken)
|
||||
}
|
||||
}
|
||||
|
||||
if tryStatement.FinallyBlock != nil {
|
||||
if finallyKeyword := astnav.FindChildOfKind(node, ast.KindFinallyKeyword, sourceFile); finallyKeyword != nil {
|
||||
keywords = append(keywords, finallyKeyword)
|
||||
}
|
||||
}
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
func getSwitchCaseDefaultOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
switchStatement := node.AsSwitchStatement()
|
||||
|
||||
var keywords []*ast.Node
|
||||
token := lsutil.GetFirstToken(node, sourceFile)
|
||||
if token.Kind == ast.KindSwitchKeyword {
|
||||
keywords = append(keywords, token)
|
||||
}
|
||||
|
||||
clauses := switchStatement.CaseBlock.AsCaseBlock().Clauses
|
||||
for _, clause := range clauses.Nodes {
|
||||
clauseToken := lsutil.GetFirstToken(clause.AsNode(), sourceFile)
|
||||
if clauseToken.Kind == ast.KindCaseKeyword || clauseToken.Kind == ast.KindDefaultKeyword {
|
||||
keywords = append(keywords, clauseToken)
|
||||
}
|
||||
|
||||
breakAndContinueStatements := aggregateAllBreakAndContinueStatements(clause, sourceFile)
|
||||
for _, statement := range breakAndContinueStatements {
|
||||
if statement.Kind == ast.KindBreakStatement && ownsBreakOrContinueStatement(switchStatement.AsNode(), statement) {
|
||||
keywords = append(keywords, lsutil.GetFirstToken(statement, sourceFile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
func aggregateAllBreakAndContinueStatements(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
if ast.IsBreakOrContinueStatement(node) {
|
||||
return []*ast.Node{node}
|
||||
}
|
||||
if ast.IsFunctionLike(node) {
|
||||
return nil
|
||||
}
|
||||
return flatMapChildren(node, sourceFile, aggregateAllBreakAndContinueStatements)
|
||||
}
|
||||
|
||||
func ownsBreakOrContinueStatement(owner *ast.Node, statement *ast.Node) bool {
|
||||
actualOwner := getBreakOrContinueOwner(statement)
|
||||
if actualOwner == nil {
|
||||
return false
|
||||
}
|
||||
return actualOwner == owner
|
||||
}
|
||||
|
||||
func getBreakOrContinueOwner(statement *ast.Node) *ast.Node {
|
||||
return ast.FindAncestorOrQuit(statement, func(node *ast.Node) ast.FindAncestorResult {
|
||||
switch node.Kind {
|
||||
case ast.KindSwitchStatement:
|
||||
if statement.Kind == ast.KindContinueStatement {
|
||||
return ast.FindAncestorFalse
|
||||
}
|
||||
fallthrough
|
||||
case ast.KindForStatement,
|
||||
ast.KindForInStatement,
|
||||
ast.KindForOfStatement,
|
||||
ast.KindWhileStatement,
|
||||
ast.KindDoStatement:
|
||||
// If the statement is labeled, check if the node is labeled by the statement's label.
|
||||
if statement.Label() == nil || isLabeledBy(node, statement.Label().Text()) {
|
||||
return ast.FindAncestorTrue
|
||||
}
|
||||
return ast.FindAncestorFalse
|
||||
default:
|
||||
// Don't cross function boundaries.
|
||||
if ast.IsFunctionLike(node) {
|
||||
return ast.FindAncestorQuit
|
||||
}
|
||||
return ast.FindAncestorFalse
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Whether or not a 'node' is preceded by a label of the given string.
|
||||
// Note: 'node' cannot be a SourceFile.
|
||||
func isLabeledBy(node *ast.Node, labelName string) bool {
|
||||
return ast.FindAncestorOrQuit(node.Parent, func(owner *ast.Node) ast.FindAncestorResult {
|
||||
if !ast.IsLabeledStatement(owner) {
|
||||
return ast.FindAncestorQuit
|
||||
}
|
||||
if owner.Label().Text() == labelName {
|
||||
return ast.FindAncestorTrue
|
||||
}
|
||||
return ast.FindAncestorFalse
|
||||
}) != nil
|
||||
}
|
||||
|
||||
func getBreakOrContinueStatementOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
if owner := getBreakOrContinueOwner(node); owner != nil {
|
||||
switch owner.Kind {
|
||||
case ast.KindForStatement, ast.KindForInStatement, ast.KindForOfStatement, ast.KindDoStatement, ast.KindWhileStatement:
|
||||
return getLoopBreakContinueOccurrences(owner, sourceFile)
|
||||
case ast.KindSwitchStatement:
|
||||
return getSwitchCaseDefaultOccurrences(owner, sourceFile)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLoopBreakContinueOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
var keywords []*ast.Node
|
||||
|
||||
token := lsutil.GetFirstToken(node, sourceFile)
|
||||
if token.Kind == ast.KindForKeyword || token.Kind == ast.KindDoKeyword || token.Kind == ast.KindWhileKeyword {
|
||||
keywords = append(keywords, token)
|
||||
if node.Kind == ast.KindDoStatement {
|
||||
loopTokens := getChildrenFromNonJSDocNode(node, sourceFile)
|
||||
for i := len(loopTokens) - 1; i >= 0; i-- {
|
||||
if loopTokens[i].Kind == ast.KindWhileKeyword {
|
||||
keywords = append(keywords, loopTokens[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
breakAndContinueStatements := aggregateAllBreakAndContinueStatements(node, sourceFile)
|
||||
for _, statement := range breakAndContinueStatements {
|
||||
token := lsutil.GetFirstToken(statement, sourceFile)
|
||||
if ownsBreakOrContinueStatement(node, statement) && (token.Kind == ast.KindBreakKeyword || token.Kind == ast.KindContinueKeyword) {
|
||||
keywords = append(keywords, token)
|
||||
}
|
||||
}
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
func getAsyncAndAwaitOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
fun := ast.GetContainingFunction(node)
|
||||
if fun == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var keywords []*ast.Node
|
||||
|
||||
for _, modifier := range fun.ModifierNodes() {
|
||||
if modifier.Kind == ast.KindAsyncKeyword {
|
||||
keywords = append(keywords, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
fun.ForEachChild(func(child *ast.Node) bool {
|
||||
traverseWithoutCrossingFunction(child, sourceFile, func(child *ast.Node) {
|
||||
if ast.IsAwaitExpression(child) {
|
||||
token := lsutil.GetFirstToken(child, sourceFile)
|
||||
if token.Kind == ast.KindAwaitKeyword {
|
||||
keywords = append(keywords, token)
|
||||
}
|
||||
}
|
||||
})
|
||||
return false // continue traversal
|
||||
})
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
func getYieldOccurrences(node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
parentFunc := ast.FindAncestor(node.Parent, ast.IsFunctionLike)
|
||||
if parentFunc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var keywords []*ast.Node
|
||||
|
||||
parentFunc.ForEachChild(func(child *ast.Node) bool {
|
||||
traverseWithoutCrossingFunction(child, sourceFile, func(child *ast.Node) {
|
||||
if ast.IsYieldExpression(child) {
|
||||
token := lsutil.GetFirstToken(child, sourceFile)
|
||||
if token.Kind == ast.KindYieldKeyword {
|
||||
keywords = append(keywords, token)
|
||||
}
|
||||
}
|
||||
})
|
||||
return false // continue traversal
|
||||
})
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
func traverseWithoutCrossingFunction(node *ast.Node, sourceFile *ast.SourceFile, cb func(*ast.Node)) {
|
||||
cb(node)
|
||||
if !ast.IsFunctionLike(node) && !ast.IsClassLike(node) && !ast.IsInterfaceDeclaration(node) && !ast.IsModuleDeclaration(node) && !ast.IsTypeAliasDeclaration(node) && !ast.IsTypeNode(node) {
|
||||
node.ForEachChild(func(child *ast.Node) bool {
|
||||
traverseWithoutCrossingFunction(child, sourceFile, cb)
|
||||
return false // continue traversal
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getModifierOccurrences(kind ast.Kind, node *ast.Node, sourceFile *ast.SourceFile) []*ast.Node {
|
||||
var result []*ast.Node
|
||||
|
||||
nodesToSearch := getNodesToSearchForModifier(node, ast.ModifierToFlag(kind))
|
||||
for _, n := range nodesToSearch {
|
||||
modifier := findModifier(n, kind)
|
||||
if modifier != nil {
|
||||
result = append(result, modifier)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getNodesToSearchForModifier(declaration *ast.Node, modifierFlag ast.ModifierFlags) []*ast.Node {
|
||||
var result []*ast.Node
|
||||
|
||||
container := declaration.Parent
|
||||
if container == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Types of node whose children might have modifiers.
|
||||
switch container.Kind {
|
||||
case ast.KindModuleBlock, ast.KindSourceFile, ast.KindBlock, ast.KindCaseClause, ast.KindDefaultClause:
|
||||
// Container is either a class declaration or the declaration is a classDeclaration
|
||||
if (modifierFlag&ast.ModifierFlagsAbstract) != 0 && ast.IsClassDeclaration(declaration) {
|
||||
return append(append(result, declaration.Members()...), declaration)
|
||||
} else {
|
||||
return append(result, container.Statements()...)
|
||||
}
|
||||
case ast.KindConstructor, ast.KindMethodDeclaration, ast.KindFunctionDeclaration:
|
||||
// Parameters and, if inside a class, also class members
|
||||
result = append(result, container.Parameters()...)
|
||||
if ast.IsClassLike(container.Parent) {
|
||||
result = append(result, container.Parent.Members()...)
|
||||
}
|
||||
return result
|
||||
case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindTypeLiteral:
|
||||
nodes := container.Members()
|
||||
result = append(result, nodes...)
|
||||
// If we're an accessibility modifier, we're in an instance member and should search
|
||||
// the constructor's parameter list for instance members as well.
|
||||
if (modifierFlag & (ast.ModifierFlagsAccessibilityModifier | ast.ModifierFlagsReadonly)) != 0 {
|
||||
var constructor *ast.Node
|
||||
|
||||
for _, member := range nodes {
|
||||
if ast.IsConstructorDeclaration(member) {
|
||||
constructor = member
|
||||
break
|
||||
}
|
||||
}
|
||||
if constructor != nil {
|
||||
result = append(result, constructor.Parameters()...)
|
||||
}
|
||||
} else if (modifierFlag & ast.ModifierFlagsAbstract) != 0 {
|
||||
result = append(result, container)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
// Syntactically invalid positions or unsupported containers
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func findModifier(node *ast.Node, kind ast.Kind) *ast.Node {
|
||||
for _, modifier := range node.ModifierNodes() {
|
||||
if modifier.Kind == kind {
|
||||
return modifier
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
382
tools/tsgo/internal/ls/file_rename.go
Normal file
382
tools/tsgo/internal/ls/file_rename.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/change"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/modulespecifiers"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/tsoptions"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
type pathUpdater func(path string) (string, bool)
|
||||
|
||||
type toImport struct {
|
||||
newFileName string
|
||||
updated bool
|
||||
}
|
||||
|
||||
func (l *LanguageService) GetEditsForFileRename(ctx context.Context, oldURI lsproto.DocumentUri, newURI lsproto.DocumentUri) []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile {
|
||||
program := l.GetProgram()
|
||||
oldPath := oldURI.FileName()
|
||||
newPath := newURI.FileName()
|
||||
|
||||
oldToNew := l.createPathUpdater(oldPath, newPath)
|
||||
|
||||
changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters)
|
||||
l.updateTsconfigFiles(program, changeTracker, oldToNew, oldPath, newPath)
|
||||
l.updateImportsForFileRename(program, changeTracker, oldToNew)
|
||||
|
||||
var documentChanges []lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile
|
||||
|
||||
// When renaming e.g. `foo.d.css.ts` -> `bar.d.css.ts`, also rename `foo.css` -> `bar.css` if it exists.
|
||||
if tspath.IsDeclarationFileName(oldPath) && tspath.IsDeclarationFileName(newPath) {
|
||||
dtsExt := tspath.GetDeclarationFileExtension(oldPath)
|
||||
originalExtensions := tspath.GetPossibleOriginalInputExtensionForExtension(dtsExt)
|
||||
for _, ext := range originalExtensions {
|
||||
oldOriginalPath := tspath.ChangeFullExtension(oldPath, ext)
|
||||
if l.host.FileExists(oldOriginalPath) {
|
||||
newDtsExt := tspath.GetDeclarationFileExtension(oldPath)
|
||||
newOriginalExtensions := tspath.GetPossibleOriginalInputExtensionForExtension(newDtsExt)
|
||||
if slices.Contains(newOriginalExtensions, ext) {
|
||||
newOriginalPath := tspath.ChangeFullExtension(newPath, ext)
|
||||
documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{
|
||||
RenameFile: &lsproto.RenameFile{
|
||||
OldUri: lsconv.FileNameToDocumentURI(oldOriginalPath),
|
||||
NewUri: lsconv.FileNameToDocumentURI(newOriginalPath),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for fileName, edits := range changeTracker.GetChanges() {
|
||||
uri := lsconv.FileNameToDocumentURI(fileName)
|
||||
lspEdits := make([]lsproto.TextEditOrAnnotatedTextEditOrSnippetTextEdit, 0, len(edits))
|
||||
for _, edit := range edits {
|
||||
lspEdits = append(lspEdits, lsproto.TextEditOrAnnotatedTextEditOrSnippetTextEdit{
|
||||
TextEdit: edit,
|
||||
})
|
||||
}
|
||||
documentChanges = append(documentChanges, lsproto.TextDocumentEditOrCreateFileOrRenameFileOrDeleteFile{
|
||||
TextDocumentEdit: &lsproto.TextDocumentEdit{
|
||||
TextDocument: lsproto.OptionalVersionedTextDocumentIdentifier{Uri: uri},
|
||||
Edits: lspEdits,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return documentChanges
|
||||
}
|
||||
|
||||
func (l *LanguageService) createPathUpdater(oldPath string, newPath string) pathUpdater {
|
||||
compareOptions := tspath.ComparePathsOptions{UseCaseSensitiveFileNames: l.UseCaseSensitiveFileNames()}
|
||||
return func(path string) (string, bool) {
|
||||
if tspath.ComparePaths(path, oldPath, compareOptions) == 0 {
|
||||
return newPath, true
|
||||
}
|
||||
if tspath.StartsWithDirectory(path, oldPath, l.UseCaseSensitiveFileNames()) {
|
||||
return newPath + path[len(oldPath):], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LanguageService) updateTsconfigFiles(program *compiler.Program, changeTracker *change.Tracker, oldToNew pathUpdater, oldPath string, newPath string) {
|
||||
commandLine := program.CommandLine()
|
||||
if commandLine == nil || commandLine.ConfigFile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
configFile := commandLine.ConfigFile.SourceFile
|
||||
if configFile == nil {
|
||||
return
|
||||
}
|
||||
configDir := tspath.GetDirectoryPath(configFile.FileName())
|
||||
jsonObjectLiteral := getTsConfigObjectLiteralExpression(configFile)
|
||||
if jsonObjectLiteral == nil {
|
||||
return
|
||||
}
|
||||
|
||||
forEachObjectProperty(jsonObjectLiteral, func(property *ast.PropertyAssignment, propertyName string) {
|
||||
switch propertyName {
|
||||
case "files", "include", "exclude":
|
||||
foundExactMatch := updatePathsProperty(configFile, configDir, property, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames())
|
||||
if foundExactMatch || propertyName != "include" || !ast.IsArrayLiteralExpression(property.Initializer) {
|
||||
return
|
||||
}
|
||||
if oldSpec, isDefault := commandLine.GetMatchedIncludeSpec(oldPath); oldSpec != "" && !isDefault {
|
||||
if newSpec, _ := commandLine.GetMatchedIncludeSpec(newPath); newSpec == "" {
|
||||
elements := property.Initializer.Elements()
|
||||
if len(elements) > 0 {
|
||||
changeTracker.InsertNodeAfter(
|
||||
configFile,
|
||||
elements[len(elements)-1],
|
||||
changeTracker.NodeFactory.NewStringLiteral(relativePathFromDirectory(configDir, newPath, l.UseCaseSensitiveFileNames()), ast.TokenFlagsNone),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
case "compilerOptions":
|
||||
if !ast.IsObjectLiteralExpression(property.Initializer) {
|
||||
return
|
||||
}
|
||||
forEachObjectProperty(property.Initializer.AsObjectLiteralExpression(), func(property *ast.PropertyAssignment, propertyName string) {
|
||||
option := tsoptions.CommandLineCompilerOptionsMap.Get(propertyName)
|
||||
if option != nil {
|
||||
elementOption := option.Elements()
|
||||
if option.IsFilePath || (option.Kind == tsoptions.CommandLineOptionTypeList && elementOption != nil && elementOption.IsFilePath) {
|
||||
updatePathsProperty(configFile, configDir, property, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if propertyName != "paths" || !ast.IsObjectLiteralExpression(property.Initializer) {
|
||||
return
|
||||
}
|
||||
forEachObjectProperty(property.Initializer.AsObjectLiteralExpression(), func(pathsProperty *ast.PropertyAssignment, _ string) {
|
||||
if !ast.IsArrayLiteralExpression(pathsProperty.Initializer) {
|
||||
return
|
||||
}
|
||||
for _, element := range pathsProperty.Initializer.Elements() {
|
||||
tryUpdateConfigString(configFile, configDir, element, changeTracker, oldToNew, l.converters, l.UseCaseSensitiveFileNames())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func updatePathsProperty(configFile *ast.SourceFile, configDir string, property *ast.PropertyAssignment, changeTracker *change.Tracker, oldToNew pathUpdater, converters *lsconv.Converters, useCaseSensitiveFileNames bool) bool {
|
||||
elements := []*ast.Node{property.Initializer}
|
||||
if ast.IsArrayLiteralExpression(property.Initializer) {
|
||||
elements = property.Initializer.Elements()
|
||||
}
|
||||
|
||||
foundExactMatch := false
|
||||
for _, element := range elements {
|
||||
foundExactMatch = tryUpdateConfigString(configFile, configDir, element, changeTracker, oldToNew, converters, useCaseSensitiveFileNames) || foundExactMatch
|
||||
}
|
||||
return foundExactMatch
|
||||
}
|
||||
|
||||
func tryUpdateConfigString(configFile *ast.SourceFile, configDir string, element *ast.Node, changeTracker *change.Tracker, oldToNew pathUpdater, converters *lsconv.Converters, useCaseSensitiveFileNames bool) bool {
|
||||
if !ast.IsStringLiteral(element) {
|
||||
return false
|
||||
}
|
||||
|
||||
elementFileName := tspath.NormalizePath(tspath.CombinePaths(configDir, element.Text()))
|
||||
updated, ok := oldToNew(elementFileName)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
changeTracker.ReplaceRangeWithText(configFile, lsproto.Range{
|
||||
Start: converters.PositionToLineAndCharacter(configFile, core.TextPos(scanner.GetTokenPosOfNode(element, configFile, false)+1)),
|
||||
End: converters.PositionToLineAndCharacter(configFile, core.TextPos(element.End()-1)),
|
||||
}, relativePathFromDirectory(configDir, updated, useCaseSensitiveFileNames))
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *LanguageService) updateRelativePath(oldToNew pathUpdater, oldImportFromPath, newImportFromPath, relativeSpecifier string) string {
|
||||
oldAbsolute := tspath.NormalizePath(tspath.CombinePaths(tspath.GetDirectoryPath(oldImportFromPath), relativeSpecifier))
|
||||
newAbsolute, ok := oldToNew(oldAbsolute)
|
||||
if !ok {
|
||||
newAbsolute = oldAbsolute
|
||||
}
|
||||
return relativeImportPathFromDirectory(tspath.GetDirectoryPath(newImportFromPath), newAbsolute, l.UseCaseSensitiveFileNames())
|
||||
}
|
||||
|
||||
func (l *LanguageService) updateImportsForFileRename(program *compiler.Program, changeTracker *change.Tracker, oldToNew pathUpdater) {
|
||||
allFiles := program.GetSourceFiles()
|
||||
checker, done := program.GetTypeChecker(context.Background())
|
||||
defer done()
|
||||
moduleSpecifierPreferences := l.UserPreferences().ModuleSpecifierPreferences()
|
||||
|
||||
for _, sourceFile := range allFiles {
|
||||
oldFileName := sourceFile.FileName()
|
||||
newFromOld, fileMoved := oldToNew(sourceFile.FileName())
|
||||
newImportFromPath := sourceFile.FileName()
|
||||
if fileMoved {
|
||||
newImportFromPath = newFromOld
|
||||
}
|
||||
|
||||
for _, ref := range sourceFile.ReferencedFiles {
|
||||
if !tspath.IsExternalModuleNameRelative(ref.FileName) {
|
||||
continue
|
||||
}
|
||||
updated := l.updateRelativePath(oldToNew, oldFileName, newImportFromPath, ref.FileName)
|
||||
if updated != ref.FileName {
|
||||
changeTracker.ReplaceRangeWithText(sourceFile, l.converters.ToLSPRange(sourceFile, ref.TextRange), updated)
|
||||
}
|
||||
}
|
||||
|
||||
for _, importStringLiteral := range sourceFile.Imports() {
|
||||
updated := l.getUpdatedImportSpecifier(program, checker, sourceFile, importStringLiteral, oldToNew, newImportFromPath, fileMoved, moduleSpecifierPreferences)
|
||||
if updated != "" && updated != importStringLiteral.Text() {
|
||||
changeTracker.ReplaceRangeWithText(sourceFile, l.converters.ToLSPRange(sourceFile, createStringTextRange(sourceFile, importStringLiteral)), updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We assume the source file did not move to a different program.
|
||||
func (l *LanguageService) getUpdatedImportSpecifier(
|
||||
program *compiler.Program,
|
||||
checker *checker.Checker,
|
||||
sourceFile *ast.SourceFile, // old importing source file
|
||||
importLiteral *ast.StringLiteralLike,
|
||||
oldToNew pathUpdater,
|
||||
newImportFromPath string,
|
||||
importingSourceFileMoved bool,
|
||||
userPreferences modulespecifiers.UserPreferences,
|
||||
) string {
|
||||
importedModuleSymbol := checker.GetSymbolAtLocation(importLiteral)
|
||||
if isAmbientModuleSymbol(importedModuleSymbol) {
|
||||
return ""
|
||||
}
|
||||
|
||||
target := getSourceFileToImport(program, sourceFile, importLiteral, oldToNew)
|
||||
|
||||
if target == nil {
|
||||
// First fall back: try every file in the program to see if any of them would match the import specifier, and if so, obtain the updated specifier for that file.
|
||||
if updated := getUpdatedImportSpecifierFromMovedSourceFiles(program, sourceFile, importLiteral, oldToNew, newImportFromPath, userPreferences); updated != "" && updated != importLiteral.Text() {
|
||||
return updated
|
||||
}
|
||||
// Fall back to a regular path update for unresolved module.
|
||||
if tspath.IsExternalModuleNameRelative(importLiteral.Text()) {
|
||||
return l.updateRelativePath(oldToNew, sourceFile.FileName(), newImportFromPath, importLiteral.Text())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Optimization: neither the importing or imported file changed.
|
||||
if !target.updated && !(importingSourceFileMoved && tspath.IsExternalModuleNameRelative(importLiteral.Text())) {
|
||||
return ""
|
||||
}
|
||||
|
||||
updated := modulespecifiers.UpdateModuleSpecifier(
|
||||
program.Options(),
|
||||
program,
|
||||
sourceFile,
|
||||
newImportFromPath,
|
||||
importLiteral.Text(),
|
||||
target.newFileName,
|
||||
userPreferences,
|
||||
modulespecifiers.ModuleSpecifierOptions{
|
||||
OverrideImportMode: program.GetModeForUsageLocation(sourceFile, importLiteral),
|
||||
},
|
||||
)
|
||||
return updated
|
||||
}
|
||||
|
||||
func getSourceFileToImport(
|
||||
program *compiler.Program,
|
||||
sourceFile *ast.SourceFile,
|
||||
importLiteral *ast.StringLiteralLike,
|
||||
oldToNew pathUpdater,
|
||||
) *toImport {
|
||||
if resolved := program.GetResolvedModuleFromModuleSpecifier(sourceFile, importLiteral); resolved != nil && resolved.ResolvedFileName != "" {
|
||||
oldFileName := resolved.ResolvedFileName
|
||||
if newFileName, ok := oldToNew(oldFileName); ok {
|
||||
return &toImport{newFileName: newFileName, updated: true}
|
||||
}
|
||||
return &toImport{newFileName: oldFileName, updated: false}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// As a fall back for unresolved modules, we'll check all files in the program to see if any of them would match
|
||||
// the import specifier, and if so, we'll obtain the updated specifier for that file.
|
||||
func getUpdatedImportSpecifierFromMovedSourceFiles(program *compiler.Program, sourceFile *ast.SourceFile, importLiteral *ast.StringLiteralLike, oldToNew pathUpdater, importingSourceFileName string, userPreferences modulespecifiers.UserPreferences) string {
|
||||
resolutionMode := program.GetModeForUsageLocation(sourceFile, importLiteral)
|
||||
for _, candidate := range program.GetSourceFiles() {
|
||||
newFileName, ok := oldToNew(candidate.FileName())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
oldSpecifier := modulespecifiers.UpdateModuleSpecifier(
|
||||
program.Options(),
|
||||
program,
|
||||
sourceFile,
|
||||
importingSourceFileName,
|
||||
importLiteral.Text(),
|
||||
candidate.FileName(),
|
||||
userPreferences,
|
||||
modulespecifiers.ModuleSpecifierOptions{
|
||||
OverrideImportMode: resolutionMode,
|
||||
},
|
||||
)
|
||||
if oldSpecifier != importLiteral.Text() {
|
||||
continue
|
||||
}
|
||||
|
||||
return modulespecifiers.UpdateModuleSpecifier(
|
||||
program.Options(),
|
||||
program,
|
||||
sourceFile,
|
||||
importingSourceFileName,
|
||||
importLiteral.Text(),
|
||||
newFileName,
|
||||
userPreferences,
|
||||
modulespecifiers.ModuleSpecifierOptions{
|
||||
OverrideImportMode: resolutionMode,
|
||||
},
|
||||
)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func createStringTextRange(sourceFile *ast.SourceFile, node *ast.LiteralLikeNode) core.TextRange {
|
||||
return core.NewTextRange(scanner.GetTokenPosOfNode(node, sourceFile, false)+1, node.End()-1)
|
||||
}
|
||||
|
||||
func getTsConfigObjectLiteralExpression(tsConfigSourceFile *ast.SourceFile) *ast.ObjectLiteralExpression {
|
||||
if tsConfigSourceFile != nil && tsConfigSourceFile.Statements != nil && len(tsConfigSourceFile.Statements.Nodes) > 0 {
|
||||
expression := tsConfigSourceFile.Statements.Nodes[0].Expression()
|
||||
if ast.IsObjectLiteralExpression(expression) {
|
||||
return expression.AsObjectLiteralExpression()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func forEachObjectProperty(objectLiteral *ast.ObjectLiteralExpression, cb func(property *ast.PropertyAssignment, propertyName string)) {
|
||||
if objectLiteral == nil {
|
||||
return
|
||||
}
|
||||
for _, property := range objectLiteral.Properties.Nodes {
|
||||
if !ast.IsPropertyAssignment(property) {
|
||||
continue
|
||||
}
|
||||
if name, ok := ast.TryGetTextOfPropertyName(property.Name()); ok {
|
||||
cb(property.AsPropertyAssignment(), name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func relativePathFromDirectory(fromDirectory string, to string, useCaseSensitiveFileNames bool) string {
|
||||
return tspath.GetRelativePathFromDirectory(fromDirectory, to, tspath.ComparePathsOptions{UseCaseSensitiveFileNames: useCaseSensitiveFileNames})
|
||||
}
|
||||
|
||||
func relativeImportPathFromDirectory(fromDirectory string, to string, useCaseSensitiveFileNames bool) string {
|
||||
return tspath.EnsurePathIsNonModuleName(relativePathFromDirectory(fromDirectory, to, useCaseSensitiveFileNames))
|
||||
}
|
||||
|
||||
func isAmbientModuleSymbol(symbol *ast.Symbol) bool {
|
||||
if symbol == nil {
|
||||
return false
|
||||
}
|
||||
return slices.ContainsFunc(symbol.Declarations, ast.IsModuleWithStringLiteralName)
|
||||
}
|
||||
2654
tools/tsgo/internal/ls/findallreferences.go
Normal file
2654
tools/tsgo/internal/ls/findallreferences.go
Normal file
File diff suppressed because it is too large
Load Diff
568
tools/tsgo/internal/ls/folding.go
Normal file
568
tools/tsgo/internal/ls/folding.go
Normal file
@@ -0,0 +1,568 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/debug"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.FoldingRangeResponse, error) {
|
||||
_, sourceFile := l.getProgramAndFile(documentURI)
|
||||
res := l.addNodeOutliningSpans(ctx, sourceFile)
|
||||
res = append(res, l.addRegionOutliningSpans(ctx, sourceFile)...)
|
||||
if lsproto.GetClientCapabilities(ctx).TextDocument.FoldingRange.LineFoldingOnly {
|
||||
res = l.adjustFoldingEnd(res, sourceFile)
|
||||
}
|
||||
slices.SortFunc(res, func(a, b *lsproto.FoldingRange) int {
|
||||
if c := cmp.Compare(a.StartLine, b.StartLine); c != 0 {
|
||||
return c
|
||||
}
|
||||
return cmp.Compare(*a.StartCharacter, *b.StartCharacter)
|
||||
})
|
||||
return lsproto.FoldingRangesOrNull{FoldingRanges: &res}, nil
|
||||
}
|
||||
|
||||
// adjustFoldingEnd adjusts the end line of folding ranges when the client signals lineFoldingOnly.
|
||||
// This mirrors the behavior of VS Code's built-in TypeScript extension (workaround for vscode#47240).
|
||||
// When lineFoldingOnly is true, we hide lines from startLine+1 to endLine. And to keep closing
|
||||
// brackets/braces visible, we subtract 1 from endLine when the range ends with a closing pair character.
|
||||
func (l *LanguageService) adjustFoldingEnd(ranges []*lsproto.FoldingRange, sourceFile *ast.SourceFile) []*lsproto.FoldingRange {
|
||||
sourceText := sourceFile.Text()
|
||||
result := make([]*lsproto.FoldingRange, 0, len(ranges))
|
||||
for _, r := range ranges {
|
||||
if r.EndCharacter != nil && *r.EndCharacter > 0 {
|
||||
endOffset := int(l.converters.LineAndCharacterToPosition(sourceFile, lsproto.Position{
|
||||
Line: r.EndLine,
|
||||
Character: *r.EndCharacter,
|
||||
}))
|
||||
if endOffset > 0 && endOffset <= len(sourceText) {
|
||||
foldEndChar := sourceText[endOffset-1]
|
||||
if foldEndChar == '}' || foldEndChar == ']' || foldEndChar == ')' || foldEndChar == '`' || foldEndChar == '>' {
|
||||
if r.EndLine > r.StartLine {
|
||||
r.EndLine--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (l *LanguageService) addNodeOutliningSpans(ctx context.Context, sourceFile *ast.SourceFile) []*lsproto.FoldingRange {
|
||||
depthRemaining := 40
|
||||
current := 0
|
||||
|
||||
statements := sourceFile.Statements
|
||||
n := len(statements.Nodes)
|
||||
foldingRange := make([]*lsproto.FoldingRange, 0, 40)
|
||||
for current < n {
|
||||
for current < n && !ast.IsAnyImportSyntax(statements.Nodes[current]) {
|
||||
foldingRange = append(foldingRange, visitNode(ctx, statements.Nodes[current], depthRemaining, sourceFile, l)...)
|
||||
current++
|
||||
}
|
||||
if current == n {
|
||||
break
|
||||
}
|
||||
firstImport := current
|
||||
for current < n && ast.IsAnyImportSyntax(statements.Nodes[current]) {
|
||||
foldingRange = append(foldingRange, visitNode(ctx, statements.Nodes[current], depthRemaining, sourceFile, l)...)
|
||||
current++
|
||||
}
|
||||
lastImport := current - 1
|
||||
if lastImport != firstImport {
|
||||
foldingRangeKind := lsproto.FoldingRangeKindImports
|
||||
foldingRange = append(foldingRange, createFoldingRangeFromBounds(
|
||||
ctx,
|
||||
astnav.GetStartOfNode(astnav.FindChildOfKind(statements.Nodes[firstImport],
|
||||
ast.KindImportKeyword, sourceFile), sourceFile, false /*includeJSDoc*/),
|
||||
statements.Nodes[lastImport].End(),
|
||||
foldingRangeKind,
|
||||
sourceFile,
|
||||
l,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Visit the EOF Token so that comments which aren't attached to statements are included.
|
||||
foldingRange = append(foldingRange, visitNode(ctx, sourceFile.EndOfFileToken, depthRemaining, sourceFile, l)...)
|
||||
return foldingRange
|
||||
}
|
||||
|
||||
func (l *LanguageService) addRegionOutliningSpans(ctx context.Context, sourceFile *ast.SourceFile) []*lsproto.FoldingRange {
|
||||
regions := make([]*lsproto.FoldingRange, 0, 40)
|
||||
out := make([]*lsproto.FoldingRange, 0, 40)
|
||||
lineStarts := scanner.GetECMALineStarts(sourceFile)
|
||||
for _, currentLineStart := range lineStarts {
|
||||
lineEnd := getLineEndOfPosition(sourceFile, int(currentLineStart))
|
||||
lineText := sourceFile.Text()[currentLineStart:lineEnd]
|
||||
result := parseRegionDelimiter(lineText)
|
||||
if result == nil || isInComment(sourceFile, int(currentLineStart), astnav.GetTokenAtPosition(sourceFile, int(currentLineStart))) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if result.isStart {
|
||||
commentStart := l.createLspPosition(strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//")+int(currentLineStart), sourceFile)
|
||||
foldingRangeKindRegion := lsproto.FoldingRangeKindRegion
|
||||
region := &lsproto.FoldingRange{
|
||||
StartLine: commentStart.Line,
|
||||
StartCharacter: &commentStart.Character,
|
||||
Kind: &foldingRangeKindRegion,
|
||||
}
|
||||
if supportsCollapsedText(ctx) {
|
||||
collapsedText := "#region"
|
||||
if result.name != "" {
|
||||
collapsedText = result.name
|
||||
}
|
||||
region.CollapsedText = &collapsedText
|
||||
}
|
||||
// Our spans start out with some initial data.
|
||||
// On every `#endregion`, we'll come back to these `FoldingRange`s
|
||||
// and fill in their EndLine/EndCharacter.
|
||||
regions = append(regions, region)
|
||||
} else {
|
||||
if len(regions) > 0 {
|
||||
region := regions[len(regions)-1]
|
||||
regions = regions[:len(regions)-1]
|
||||
endingPosition := l.createLspPosition(lineEnd, sourceFile)
|
||||
region.EndLine = endingPosition.Line
|
||||
region.EndCharacter = &endingPosition.Character
|
||||
out = append(out, region)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func visitNode(ctx context.Context, n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange {
|
||||
if n.Flags&ast.NodeFlagsReparsed != 0 || depthRemaining == 0 || ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
foldingRange := make([]*lsproto.FoldingRange, 0, 40)
|
||||
if (!ast.IsBinaryExpression(n) && ast.IsDeclaration(n)) || ast.IsVariableStatement(n) || ast.IsReturnStatement(n) || ast.IsCallOrNewExpression(n) || n.Kind == ast.KindEndOfFile {
|
||||
foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(ctx, n, sourceFile, l)...)
|
||||
}
|
||||
if ast.IsFunctionLike(n) && n.Parent != nil && ast.IsBinaryExpression(n.Parent) && n.Parent.AsBinaryExpression().Left != nil && ast.IsPropertyAccessExpression(n.Parent.AsBinaryExpression().Left) {
|
||||
foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(ctx, n.Parent.AsBinaryExpression().Left, sourceFile, l)...)
|
||||
}
|
||||
if ast.IsBlock(n) {
|
||||
statements := n.AsBlock().Statements
|
||||
if statements != nil {
|
||||
foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, statements.End(), sourceFile, l)...)
|
||||
}
|
||||
}
|
||||
if ast.IsModuleBlock(n) {
|
||||
statements := n.AsModuleBlock().Statements
|
||||
if statements != nil {
|
||||
foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, statements.End(), sourceFile, l)...)
|
||||
}
|
||||
}
|
||||
if ast.IsClassLike(n) || ast.IsInterfaceDeclaration(n) {
|
||||
var members *ast.NodeList
|
||||
if ast.IsClassDeclaration(n) {
|
||||
members = n.AsClassDeclaration().Members
|
||||
} else if ast.IsClassExpression(n) {
|
||||
members = n.AsClassExpression().Members
|
||||
} else {
|
||||
members = n.AsInterfaceDeclaration().Members
|
||||
}
|
||||
if members != nil {
|
||||
foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, members.End(), sourceFile, l)...)
|
||||
}
|
||||
}
|
||||
|
||||
span := getOutliningSpanForNode(ctx, n, sourceFile, l)
|
||||
if span != nil {
|
||||
foldingRange = append(foldingRange, span)
|
||||
}
|
||||
|
||||
depthRemaining--
|
||||
if ast.IsCallExpression(n) {
|
||||
depthRemaining++
|
||||
expressionNodes := visitNode(ctx, n.Expression(), depthRemaining, sourceFile, l)
|
||||
if expressionNodes != nil {
|
||||
foldingRange = append(foldingRange, expressionNodes...)
|
||||
}
|
||||
depthRemaining--
|
||||
for _, arg := range n.Arguments() {
|
||||
if arg != nil {
|
||||
foldingRange = append(foldingRange, visitNode(ctx, arg, depthRemaining, sourceFile, l)...)
|
||||
}
|
||||
}
|
||||
typeArguments := n.TypeArguments()
|
||||
for _, typeArg := range typeArguments {
|
||||
if typeArg != nil {
|
||||
foldingRange = append(foldingRange, visitNode(ctx, typeArg, depthRemaining, sourceFile, l)...)
|
||||
}
|
||||
}
|
||||
} else if ast.IsIfStatement(n) && n.AsIfStatement().ElseStatement != nil && ast.IsIfStatement(n.AsIfStatement().ElseStatement) {
|
||||
// Consider an 'else if' to be on the same depth as the 'if'.
|
||||
ifStatement := n.AsIfStatement()
|
||||
expressionNodes := visitNode(ctx, n.Expression(), depthRemaining, sourceFile, l)
|
||||
if expressionNodes != nil {
|
||||
foldingRange = append(foldingRange, expressionNodes...)
|
||||
}
|
||||
thenNode := visitNode(ctx, ifStatement.ThenStatement, depthRemaining, sourceFile, l)
|
||||
if thenNode != nil {
|
||||
foldingRange = append(foldingRange, thenNode...)
|
||||
}
|
||||
depthRemaining++
|
||||
elseNode := visitNode(ctx, ifStatement.ElseStatement, depthRemaining, sourceFile, l)
|
||||
if elseNode != nil {
|
||||
foldingRange = append(foldingRange, elseNode...)
|
||||
}
|
||||
depthRemaining--
|
||||
} else {
|
||||
visit := func(node *ast.Node) bool {
|
||||
childNode := visitNode(ctx, node, depthRemaining, sourceFile, l)
|
||||
if childNode != nil {
|
||||
foldingRange = append(foldingRange, childNode...)
|
||||
}
|
||||
return false
|
||||
}
|
||||
n.ForEachChild(visit)
|
||||
}
|
||||
depthRemaining++
|
||||
return foldingRange
|
||||
}
|
||||
|
||||
func addOutliningForLeadingCommentsForNode(ctx context.Context, n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange {
|
||||
if ast.IsJsxText(n) {
|
||||
return nil
|
||||
}
|
||||
return addOutliningForLeadingCommentsForPos(ctx, n.Pos(), sourceFile, l)
|
||||
}
|
||||
|
||||
func addOutliningForLeadingCommentsForPos(ctx context.Context, pos int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange {
|
||||
p := &printer.EmitContext{}
|
||||
foldingRange := make([]*lsproto.FoldingRange, 0, 40)
|
||||
firstSingleLineCommentStart := -1
|
||||
lastSingleLineCommentEnd := -1
|
||||
singleLineCommentCount := 0
|
||||
foldingRangeKindComment := lsproto.FoldingRangeKindComment
|
||||
|
||||
combineAndAddMultipleSingleLineComments := func() *lsproto.FoldingRange {
|
||||
// Only outline spans of two or more consecutive single line comments
|
||||
if singleLineCommentCount > 1 {
|
||||
return createFoldingRangeFromBounds(ctx, firstSingleLineCommentStart, lastSingleLineCommentEnd, foldingRangeKindComment, sourceFile, l)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
sourceText := sourceFile.Text()
|
||||
for comment := range scanner.GetLeadingCommentRanges(&printer.NewNodeFactory(p).NodeFactory, sourceText, pos) {
|
||||
commentPos := comment.Pos()
|
||||
commentEnd := comment.End()
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
switch comment.Kind {
|
||||
case ast.KindSingleLineCommentTrivia:
|
||||
// never fold region delimiters into single-line comment regions
|
||||
commentText := sourceText[commentPos:commentEnd]
|
||||
if parseRegionDelimiter(commentText) != nil {
|
||||
comments := combineAndAddMultipleSingleLineComments()
|
||||
if comments != nil {
|
||||
foldingRange = append(foldingRange, comments)
|
||||
}
|
||||
singleLineCommentCount = 0
|
||||
break
|
||||
}
|
||||
|
||||
// For single line comments, combine consecutive ones (2 or more) into
|
||||
// a single span from the start of the first till the end of the last
|
||||
if singleLineCommentCount == 0 {
|
||||
firstSingleLineCommentStart = commentPos
|
||||
}
|
||||
lastSingleLineCommentEnd = commentEnd
|
||||
singleLineCommentCount++
|
||||
break
|
||||
case ast.KindMultiLineCommentTrivia:
|
||||
comments := combineAndAddMultipleSingleLineComments()
|
||||
if comments != nil {
|
||||
foldingRange = append(foldingRange, comments)
|
||||
}
|
||||
foldingRange = append(foldingRange, createFoldingRangeFromBounds(ctx, commentPos, commentEnd, foldingRangeKindComment, sourceFile, l))
|
||||
singleLineCommentCount = 0
|
||||
break
|
||||
default:
|
||||
debug.AssertNever(comment.Kind)
|
||||
}
|
||||
}
|
||||
addedComments := combineAndAddMultipleSingleLineComments()
|
||||
if addedComments != nil {
|
||||
foldingRange = append(foldingRange, addedComments)
|
||||
}
|
||||
return foldingRange
|
||||
}
|
||||
|
||||
type regionDelimiterResult struct {
|
||||
isStart bool
|
||||
name string
|
||||
}
|
||||
|
||||
func parseRegionDelimiter(lineText string) *regionDelimiterResult {
|
||||
// We trim the leading whitespace and // without the regex since the
|
||||
// multiple potential whitespace matches can make for some gnarly backtracking behavior
|
||||
lineText = strings.TrimLeftFunc(lineText, unicode.IsSpace)
|
||||
if !strings.HasPrefix(lineText, "//") {
|
||||
return nil
|
||||
}
|
||||
lineText = strings.TrimSpace(lineText[2:])
|
||||
lineText = strings.TrimSuffix(lineText, "\r")
|
||||
if !strings.HasPrefix(lineText, "#") {
|
||||
return nil
|
||||
}
|
||||
lineText = lineText[1:]
|
||||
isStart := true
|
||||
if strings.HasPrefix(lineText, "end") {
|
||||
isStart = false
|
||||
lineText = lineText[3:]
|
||||
}
|
||||
if !strings.HasPrefix(lineText, "region") {
|
||||
return nil
|
||||
}
|
||||
lineText = lineText[6:]
|
||||
return ®ionDelimiterResult{
|
||||
isStart: isStart,
|
||||
name: strings.TrimSpace(lineText),
|
||||
}
|
||||
}
|
||||
|
||||
func getOutliningSpanForNode(ctx context.Context, n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
switch n.Kind {
|
||||
case ast.KindBlock:
|
||||
if ast.IsFunctionLike(n.Parent) {
|
||||
return functionSpan(ctx, n.Parent, n, sourceFile, l)
|
||||
}
|
||||
// Check if the block is standalone, or 'attached' to some parent statement.
|
||||
// If the latter, we want to collapse the block, but consider its hint span
|
||||
// to be the entire span of the parent.
|
||||
switch n.Parent.Kind {
|
||||
case ast.KindDoStatement, ast.KindForInStatement, ast.KindForOfStatement, ast.KindForStatement, ast.KindIfStatement, ast.KindWhileStatement, ast.KindWithStatement, ast.KindCatchClause:
|
||||
return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l)
|
||||
case ast.KindTryStatement:
|
||||
// Could be the try-block, or the finally-block.
|
||||
tryStatement := n.Parent.AsTryStatement()
|
||||
if tryStatement.TryBlock == n {
|
||||
return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l)
|
||||
} else if tryStatement.FinallyBlock == n {
|
||||
if span := spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l); span != nil {
|
||||
return span
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
// Block was a standalone block. In this case we want to only collapse
|
||||
// the span of the block, independent of any parent span.
|
||||
return createFoldingRange(ctx, l.createLspRangeFromNode(n, sourceFile), "", "")
|
||||
}
|
||||
case ast.KindModuleBlock:
|
||||
return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l)
|
||||
case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration, ast.KindCaseBlock, ast.KindTypeLiteral, ast.KindObjectBindingPattern:
|
||||
return spanForNode(ctx, n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l)
|
||||
case ast.KindTupleType:
|
||||
return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsTupleTypeNode(n.Parent) /*useFullStart*/, sourceFile, l)
|
||||
case ast.KindCaseClause, ast.KindDefaultClause:
|
||||
return spanForNodeArray(ctx, n.AsCaseOrDefaultClause().Statements, sourceFile, l)
|
||||
case ast.KindObjectLiteralExpression:
|
||||
return spanForNode(ctx, n, ast.KindOpenBraceToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l)
|
||||
case ast.KindArrayLiteralExpression:
|
||||
return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l)
|
||||
case ast.KindJsxElement, ast.KindJsxFragment:
|
||||
return spanForJSXElement(ctx, n, sourceFile, l)
|
||||
case ast.KindJsxSelfClosingElement, ast.KindJsxOpeningElement:
|
||||
return spanForJSXAttributes(ctx, n, sourceFile, l)
|
||||
case ast.KindTemplateExpression, ast.KindNoSubstitutionTemplateLiteral:
|
||||
return spanForTemplateLiteral(ctx, n, sourceFile, l)
|
||||
case ast.KindArrayBindingPattern:
|
||||
return spanForNode(ctx, n, ast.KindOpenBracketToken, !ast.IsBindingElement(n.Parent) /*useFullStart*/, sourceFile, l)
|
||||
case ast.KindArrowFunction:
|
||||
return spanForArrowFunction(ctx, n, sourceFile, l)
|
||||
case ast.KindCallExpression:
|
||||
return spanForCallExpression(ctx, n, sourceFile, l)
|
||||
case ast.KindParenthesizedExpression:
|
||||
return spanForParenthesizedExpression(ctx, n, sourceFile, l)
|
||||
case ast.KindNamedImports, ast.KindNamedExports, ast.KindImportAttributes:
|
||||
return spanForImportExportElements(ctx, n, sourceFile, l)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func spanForImportExportElements(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
var elements *ast.NodeList
|
||||
switch node.Kind {
|
||||
case ast.KindNamedImports:
|
||||
elements = node.AsNamedImports().Elements
|
||||
case ast.KindNamedExports:
|
||||
elements = node.AsNamedExports().Elements
|
||||
case ast.KindImportAttributes:
|
||||
elements = node.AsImportAttributes().Attributes
|
||||
}
|
||||
if elements == nil || len(elements.Nodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
openToken := astnav.FindChildOfKind(node, ast.KindOpenBraceToken, sourceFile)
|
||||
closeToken := astnav.FindChildOfKind(node, ast.KindCloseBraceToken, sourceFile)
|
||||
if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) {
|
||||
return nil
|
||||
}
|
||||
return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, false /*useFullStart*/, l)
|
||||
}
|
||||
|
||||
func spanForParenthesizedExpression(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
start := astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/)
|
||||
if printer.PositionsAreOnSameLine(start, node.End(), sourceFile) {
|
||||
return nil
|
||||
}
|
||||
textRange := l.createLspRangeFromBounds(start, node.End(), sourceFile)
|
||||
return createFoldingRange(ctx, textRange, "", "")
|
||||
}
|
||||
|
||||
func spanForCallExpression(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
if node.AsCallExpression().Arguments == nil || len(node.AsCallExpression().Arguments.Nodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
openToken := astnav.FindChildOfKind(node, ast.KindOpenParenToken, sourceFile)
|
||||
closeToken := astnav.FindChildOfKind(node, ast.KindCloseParenToken, sourceFile)
|
||||
if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, true /*useFullStart*/, l)
|
||||
}
|
||||
|
||||
func spanForArrowFunction(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
arrowFunctionNode := node.AsArrowFunction()
|
||||
if ast.IsBlock(arrowFunctionNode.Body) || ast.IsParenthesizedExpression(arrowFunctionNode.Body) || printer.PositionsAreOnSameLine(arrowFunctionNode.Body.Pos(), arrowFunctionNode.Body.End(), sourceFile) {
|
||||
return nil
|
||||
}
|
||||
textRange := l.createLspRangeFromBounds(arrowFunctionNode.Body.Pos(), arrowFunctionNode.Body.End(), sourceFile)
|
||||
return createFoldingRange(ctx, textRange, "", "")
|
||||
}
|
||||
|
||||
func spanForTemplateLiteral(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
if node.Kind == ast.KindNoSubstitutionTemplateLiteral && len(node.Text()) == 0 {
|
||||
return nil
|
||||
}
|
||||
return createFoldingRangeFromBounds(ctx, astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l)
|
||||
}
|
||||
|
||||
func spanForJSXElement(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
if node.Kind == ast.KindJsxElement {
|
||||
jsxElement := node.AsJsxElement()
|
||||
textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxElement.OpeningElement, sourceFile, false /*includeJSDoc*/), jsxElement.ClosingElement.End(), sourceFile)
|
||||
tagName := scanner.GetTextOfNode(jsxElement.OpeningElement.TagName())
|
||||
bannerText := "<" + tagName + ">...</" + tagName + ">"
|
||||
return createFoldingRange(ctx, textRange, "", bannerText)
|
||||
}
|
||||
// JsxFragment
|
||||
jsxFragment := node.AsJsxFragment()
|
||||
textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxFragment.OpeningFragment, sourceFile, false /*includeJSDoc*/), jsxFragment.ClosingFragment.End(), sourceFile)
|
||||
return createFoldingRange(ctx, textRange, "", "<>...</>")
|
||||
}
|
||||
|
||||
func spanForJSXAttributes(ctx context.Context, node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
var attributes *ast.JsxAttributesNode
|
||||
if node.Kind == ast.KindJsxSelfClosingElement {
|
||||
attributes = node.AsJsxSelfClosingElement().Attributes
|
||||
} else {
|
||||
attributes = node.AsJsxOpeningElement().Attributes
|
||||
}
|
||||
if len(attributes.Properties()) == 0 {
|
||||
return nil
|
||||
}
|
||||
return createFoldingRangeFromBounds(ctx, astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l)
|
||||
}
|
||||
|
||||
func spanForNodeArray(ctx context.Context, statements *ast.NodeList, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
if statements != nil && len(statements.Nodes) != 0 {
|
||||
return createFoldingRange(ctx, l.createLspRangeFromBounds(statements.Pos(), statements.End(), sourceFile), "", "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func spanForNode(ctx context.Context, node *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
closeBrace := ast.KindCloseBraceToken
|
||||
if open != ast.KindOpenBraceToken {
|
||||
closeBrace = ast.KindCloseBracketToken
|
||||
}
|
||||
openToken := astnav.FindChildOfKind(node, open, sourceFile)
|
||||
closeToken := astnav.FindChildOfKind(node, closeBrace, sourceFile)
|
||||
if openToken != nil && closeToken != nil {
|
||||
return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, useFullStart, l)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rangeBetweenTokens(ctx context.Context, openToken *ast.Node, closeToken *ast.Node, sourceFile *ast.SourceFile, useFullStart bool, l *LanguageService) *lsproto.FoldingRange {
|
||||
var textRange lsproto.Range
|
||||
if useFullStart {
|
||||
textRange = l.createLspRangeFromBounds(openToken.Pos(), closeToken.End(), sourceFile)
|
||||
} else {
|
||||
textRange = l.createLspRangeFromBounds(astnav.GetStartOfNode(openToken, sourceFile, false /*includeJSDoc*/), closeToken.End(), sourceFile)
|
||||
}
|
||||
return createFoldingRange(ctx, textRange, "", "")
|
||||
}
|
||||
|
||||
func supportsCollapsedText(ctx context.Context) bool {
|
||||
return lsproto.GetClientCapabilities(ctx).TextDocument.FoldingRange.FoldingRange.CollapsedText
|
||||
}
|
||||
|
||||
func createFoldingRange(ctx context.Context, textRange lsproto.Range, foldingRangeKind lsproto.FoldingRangeKind, collapsedText string) *lsproto.FoldingRange {
|
||||
var kind *lsproto.FoldingRangeKind
|
||||
if foldingRangeKind != "" {
|
||||
kind = &foldingRangeKind
|
||||
}
|
||||
result := &lsproto.FoldingRange{
|
||||
StartLine: textRange.Start.Line,
|
||||
StartCharacter: &textRange.Start.Character,
|
||||
EndLine: textRange.End.Line,
|
||||
EndCharacter: &textRange.End.Character,
|
||||
Kind: kind,
|
||||
}
|
||||
if collapsedText != "" && supportsCollapsedText(ctx) {
|
||||
result.CollapsedText = &collapsedText
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func createFoldingRangeFromBounds(ctx context.Context, pos int, end int, foldingRangeKind lsproto.FoldingRangeKind, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
return createFoldingRange(ctx, l.createLspRangeFromBounds(pos, end, sourceFile), foldingRangeKind, "")
|
||||
}
|
||||
|
||||
func functionSpan(ctx context.Context, node *ast.Node, body *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange {
|
||||
openToken := tryGetFunctionOpenToken(node, body, sourceFile)
|
||||
closeToken := astnav.FindChildOfKind(body, ast.KindCloseBraceToken, sourceFile)
|
||||
if openToken != nil && closeToken != nil {
|
||||
return rangeBetweenTokens(ctx, openToken, closeToken, sourceFile, true /*useFullStart*/, l)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tryGetFunctionOpenToken(node *ast.SignatureDeclaration, body *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
|
||||
if isNodeArrayMultiLine(node.Parameters(), sourceFile) {
|
||||
openParenToken := astnav.FindChildOfKind(node, ast.KindOpenParenToken, sourceFile)
|
||||
if openParenToken != nil {
|
||||
return openParenToken
|
||||
}
|
||||
}
|
||||
return astnav.FindChildOfKind(body, ast.KindOpenBraceToken, sourceFile)
|
||||
}
|
||||
|
||||
func isNodeArrayMultiLine(list []*ast.Node, sourceFile *ast.SourceFile) bool {
|
||||
if len(list) == 0 {
|
||||
return false
|
||||
}
|
||||
return !printer.PositionsAreOnSameLine(list[0].Pos(), list[len(list)-1].End(), sourceFile)
|
||||
}
|
||||
181
tools/tsgo/internal/ls/format.go
Normal file
181
tools/tsgo/internal/ls/format.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"iter"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/format"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
func (l *LanguageService) toLSProtoTextEdits(file *ast.SourceFile, changes []core.TextChange) []*lsproto.TextEdit {
|
||||
result := make([]*lsproto.TextEdit, 0, len(changes))
|
||||
for _, c := range changes {
|
||||
result = append(result, &lsproto.TextEdit{
|
||||
NewText: c.NewText,
|
||||
Range: l.createLspRangeFromBounds(c.Pos(), c.End(), file),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (l *LanguageService) ProvideFormatDocument(
|
||||
ctx context.Context,
|
||||
documentURI lsproto.DocumentUri,
|
||||
options *lsproto.FormattingOptions,
|
||||
) (lsproto.DocumentFormattingResponse, error) {
|
||||
if l.UserPreferences().EnableFormatting.IsFalse() {
|
||||
return lsproto.TextEditsOrNull{}, nil
|
||||
}
|
||||
_, file := l.getProgramAndFile(documentURI)
|
||||
formatOpts := lsutil.FromLSFormatOptions(l.FormatOptions(), options)
|
||||
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForDocument(
|
||||
ctx,
|
||||
file,
|
||||
formatOpts,
|
||||
))
|
||||
return lsproto.TextEditsOrNull{TextEdits: &edits}, nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) ProvideFormatDocumentRange(
|
||||
ctx context.Context,
|
||||
documentURI lsproto.DocumentUri,
|
||||
options *lsproto.FormattingOptions,
|
||||
r lsproto.Range,
|
||||
) (lsproto.DocumentRangeFormattingResponse, error) {
|
||||
if l.UserPreferences().EnableFormatting.IsFalse() {
|
||||
return lsproto.TextEditsOrNull{}, nil
|
||||
}
|
||||
_, file := l.getProgramAndFile(documentURI)
|
||||
formatOpts := lsutil.FromLSFormatOptions(l.FormatOptions(), options)
|
||||
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForRange(
|
||||
ctx,
|
||||
file,
|
||||
formatOpts,
|
||||
l.converters.FromLSPRange(file, r),
|
||||
))
|
||||
return lsproto.TextEditsOrNull{TextEdits: &edits}, nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) ProvideFormatDocumentOnType(
|
||||
ctx context.Context,
|
||||
documentURI lsproto.DocumentUri,
|
||||
options *lsproto.FormattingOptions,
|
||||
position lsproto.Position,
|
||||
character string,
|
||||
) (lsproto.DocumentOnTypeFormattingResponse, error) {
|
||||
if l.UserPreferences().EnableFormatting.IsFalse() {
|
||||
return lsproto.TextEditsOrNull{}, nil
|
||||
}
|
||||
_, file := l.getProgramAndFile(documentURI)
|
||||
formatOpts := lsutil.FromLSFormatOptions(l.FormatOptions(), options)
|
||||
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsAfterKeystroke(
|
||||
ctx,
|
||||
file,
|
||||
formatOpts,
|
||||
int(l.converters.LineAndCharacterToPosition(file, position)),
|
||||
character,
|
||||
))
|
||||
return lsproto.TextEditsOrNull{TextEdits: &edits}, nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) getFormattingEditsForRange(
|
||||
ctx context.Context,
|
||||
file *ast.SourceFile,
|
||||
options lsutil.FormatCodeSettings,
|
||||
r core.TextRange,
|
||||
) []core.TextChange {
|
||||
ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter)
|
||||
return format.FormatSelection(ctx, file, r.Pos(), r.End())
|
||||
}
|
||||
|
||||
func (l *LanguageService) getFormattingEditsForDocument(
|
||||
ctx context.Context,
|
||||
file *ast.SourceFile,
|
||||
options lsutil.FormatCodeSettings,
|
||||
) []core.TextChange {
|
||||
ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter)
|
||||
return format.FormatDocument(ctx, file)
|
||||
}
|
||||
|
||||
func (l *LanguageService) getFormattingEditsAfterKeystroke(
|
||||
ctx context.Context,
|
||||
file *ast.SourceFile,
|
||||
options lsutil.FormatCodeSettings,
|
||||
position int,
|
||||
key string,
|
||||
) []core.TextChange {
|
||||
ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter)
|
||||
|
||||
tokenAtPosition := astnav.GetTokenAtPosition(file, position)
|
||||
if isInComment(file, position, tokenAtPosition) == nil {
|
||||
switch key {
|
||||
case "{":
|
||||
return format.FormatOnOpeningCurly(ctx, file, position)
|
||||
case "}":
|
||||
return format.FormatOnClosingCurly(ctx, file, position)
|
||||
case ";":
|
||||
return format.FormatOnSemicolon(ctx, file, position)
|
||||
case "\n":
|
||||
return format.FormatOnEnter(ctx, file, position)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unlike the TS implementation, this function *will not* compute default values for
|
||||
// `precedingToken` and `tokenAtPosition`.
|
||||
// It is the caller's responsibility to call `astnav.GetTokenAtPosition` to compute a default `tokenAtPosition`,
|
||||
// or `astnav.FindPrecedingToken` to compute a default `precedingToken`.
|
||||
func getRangeOfEnclosingComment(
|
||||
file *ast.SourceFile,
|
||||
position int,
|
||||
precedingToken *ast.Node,
|
||||
tokenAtPosition *ast.Node,
|
||||
) *ast.CommentRange {
|
||||
jsdoc := ast.FindAncestor(tokenAtPosition, (*ast.Node).IsJSDoc)
|
||||
if jsdoc != nil {
|
||||
tokenAtPosition = jsdoc.Parent
|
||||
}
|
||||
tokenStart := astnav.GetStartOfNode(tokenAtPosition, file, false /*includeJSDoc*/)
|
||||
if tokenStart <= position && position < tokenAtPosition.End() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Between two consecutive tokens, all comments are either trailing on the former
|
||||
// or leading on the latter (and none are in both lists).
|
||||
var trailingRangesOfPreviousToken iter.Seq[ast.CommentRange]
|
||||
if precedingToken != nil {
|
||||
trailingRangesOfPreviousToken = scanner.GetTrailingCommentRanges(&ast.NodeFactory{}, file.Text(), precedingToken.End())
|
||||
}
|
||||
leadingRangesOfNextToken := getLeadingCommentRangesOfNode(tokenAtPosition, file)
|
||||
commentRanges := core.ConcatenateSeq(trailingRangesOfPreviousToken, leadingRangesOfNextToken)
|
||||
for commentRange := range commentRanges {
|
||||
// The end marker of a single-line comment does not include the newline character.
|
||||
// In the following case where the cursor is at `^`, we are inside a comment:
|
||||
//
|
||||
// // asdf ^\n
|
||||
//
|
||||
// But for closed multi-line comments, we don't want to be inside the comment in the following case:
|
||||
//
|
||||
// /* asdf */^
|
||||
//
|
||||
// Internally, we represent the end of the comment prior to the newline and at the '/', respectively.
|
||||
//
|
||||
// However, unterminated multi-line comments lack a `/`, end at the end of the file, and *do* contain their end.
|
||||
//
|
||||
if commentRange.ContainsExclusive(position) ||
|
||||
position == commentRange.End() &&
|
||||
(commentRange.Kind == ast.KindSingleLineCommentTrivia || position == len(file.Text())) {
|
||||
return &commentRange
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
135
tools/tsgo/internal/ls/format_test.go
Normal file
135
tools/tsgo/internal/ls/format_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
)
|
||||
|
||||
// Test for issue: Panic Handling textDocument/onTypeFormatting
|
||||
// This reproduces the panic when pressing enter in an empty file
|
||||
func TestGetFormattingEditsAfterKeystroke_EmptyFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Create an empty file
|
||||
text := ""
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/index.ts",
|
||||
Path: "/index.ts",
|
||||
}, text, core.ScriptKindTS)
|
||||
|
||||
// Create language service with nil program (we're only testing the formatting function)
|
||||
langService := &LanguageService{}
|
||||
|
||||
// Test formatting after keystroke with newline character at position 0
|
||||
ctx := context.Background()
|
||||
options := lsutil.GetDefaultFormatCodeSettings()
|
||||
|
||||
// This should not panic
|
||||
edits := langService.getFormattingEditsAfterKeystroke(
|
||||
ctx,
|
||||
sourceFile,
|
||||
options,
|
||||
0, // position
|
||||
"\n",
|
||||
)
|
||||
|
||||
// Should return nil or empty edits, not panic
|
||||
_ = edits
|
||||
}
|
||||
|
||||
// Test with a simple statement
|
||||
func TestGetFormattingEditsAfterKeystroke_SimpleStatement(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Create a file with a simple statement
|
||||
text := "const x = 1"
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/index.ts",
|
||||
Path: "/index.ts",
|
||||
}, text, core.ScriptKindTS)
|
||||
|
||||
// Create language service with nil program
|
||||
langService := &LanguageService{}
|
||||
|
||||
// Test formatting after keystroke with newline character at end of statement
|
||||
ctx := context.Background()
|
||||
options := lsutil.GetDefaultFormatCodeSettings()
|
||||
|
||||
// This should not panic
|
||||
edits := langService.getFormattingEditsAfterKeystroke(
|
||||
ctx,
|
||||
sourceFile,
|
||||
options,
|
||||
len(text), // position at end of file
|
||||
"\n",
|
||||
)
|
||||
|
||||
// Should return nil or empty edits, not panic
|
||||
_ = edits
|
||||
}
|
||||
|
||||
// Test for issue: Crash in range formatting when requested on a line that is different from the containing function
|
||||
// This reproduces the panic when formatting a range inside a function body
|
||||
func TestGetFormattingEditsForRange_FunctionBody(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
text string
|
||||
startPos int
|
||||
endPos int
|
||||
}{
|
||||
{
|
||||
name: "return statement in function",
|
||||
text: "function foo() {\n return (1 + 2);\n}",
|
||||
startPos: 21, // Start of "return"
|
||||
endPos: 38, // End of ");"
|
||||
},
|
||||
{
|
||||
name: "function with newline after keyword",
|
||||
text: "function\nf() {\n}",
|
||||
startPos: 9, // After "function\n"
|
||||
endPos: 13, // Inside or after function
|
||||
},
|
||||
{
|
||||
name: "empty function body",
|
||||
text: "function f() {\n \n}",
|
||||
startPos: 15, // Inside body
|
||||
endPos: 17, // Inside body
|
||||
},
|
||||
{
|
||||
name: "after function closing brace",
|
||||
text: "function f() {\n}",
|
||||
startPos: 15, // After closing brace
|
||||
endPos: 15,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, tc.text, core.ScriptKindTS)
|
||||
|
||||
langService := &LanguageService{}
|
||||
ctx := context.Background()
|
||||
options := lsutil.GetDefaultFormatCodeSettings()
|
||||
|
||||
// This should not panic
|
||||
edits := langService.getFormattingEditsForRange(
|
||||
ctx,
|
||||
sourceFile,
|
||||
options,
|
||||
core.NewTextRange(tc.startPos, tc.endPos),
|
||||
)
|
||||
|
||||
// Should not panic
|
||||
_ = edits // Just ensuring no panic
|
||||
})
|
||||
}
|
||||
}
|
||||
25
tools/tsgo/internal/ls/host.go
Normal file
25
tools/tsgo/internal/ls/host.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ls/autoimport"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/sourcemap"
|
||||
)
|
||||
|
||||
type Host interface {
|
||||
UseCaseSensitiveFileNames() bool
|
||||
ReadFile(path string) (contents string, ok bool)
|
||||
Converters() *lsconv.Converters
|
||||
GetPreferences(activeFile string) lsutil.UserPreferences
|
||||
GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo
|
||||
AutoImportRegistry() *autoimport.Registry
|
||||
|
||||
// Used for module specifier completions.
|
||||
// ! Do not use for anything else, as this violates the principle that
|
||||
// the host is a snapshot-in-time.
|
||||
ReadDirectory(currentDir string, path string, extensions []string, excludes []string, includes []string, depth int) []string
|
||||
GetDirectories(path string) []string
|
||||
DirectoryExists(path string) bool
|
||||
FileExists(path string) bool
|
||||
}
|
||||
1133
tools/tsgo/internal/ls/hover.go
Normal file
1133
tools/tsgo/internal/ls/hover.go
Normal file
File diff suppressed because it is too large
Load Diff
767
tools/tsgo/internal/ls/importTracker.go
Normal file
767
tools/tsgo/internal/ls/importTracker.go
Normal file
@@ -0,0 +1,767 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/debug"
|
||||
)
|
||||
|
||||
type ImpExpKind int32
|
||||
|
||||
const (
|
||||
ImpExpKindUnknown ImpExpKind = iota
|
||||
ImpExpKindImport
|
||||
ImpExpKindExport
|
||||
)
|
||||
|
||||
type ImportExportSymbol struct {
|
||||
kind ImpExpKind
|
||||
symbol *ast.Symbol
|
||||
exportInfo *ExportInfo
|
||||
}
|
||||
|
||||
type ExportKind int
|
||||
|
||||
const (
|
||||
ExportKindNamed ExportKind = 0
|
||||
ExportKindDefault ExportKind = 1
|
||||
ExportKindExportEquals ExportKind = 2
|
||||
ExportKindUMD ExportKind = 3
|
||||
ExportKindModule ExportKind = 4
|
||||
)
|
||||
|
||||
type ExportInfo struct {
|
||||
exportingModuleSymbol *ast.Symbol
|
||||
exportKind ExportKind
|
||||
}
|
||||
|
||||
type LocationAndSymbol struct {
|
||||
importLocation *ast.Node
|
||||
importSymbol *ast.Symbol
|
||||
}
|
||||
|
||||
type ImportsResult struct {
|
||||
importSearches []LocationAndSymbol
|
||||
singleReferences []*ast.Node
|
||||
indirectUsers []*ast.SourceFile
|
||||
}
|
||||
|
||||
type ImportTracker func(exportSymbol *ast.Symbol, exportInfo *ExportInfo, isForRename bool) *ImportsResult
|
||||
|
||||
type ModuleReferenceKind int32
|
||||
|
||||
const (
|
||||
ModuleReferenceKindImport ModuleReferenceKind = iota
|
||||
ModuleReferenceKindReference
|
||||
ModuleReferenceKindImplicit
|
||||
)
|
||||
|
||||
// ModuleReference represents a reference to a module, either via import, <reference>, or implicit reference
|
||||
type ModuleReference struct {
|
||||
kind ModuleReferenceKind
|
||||
literal *ast.Node // for import and implicit kinds (StringLiteralLike)
|
||||
referencingFile *ast.SourceFile
|
||||
ref *ast.FileReference // for reference kind
|
||||
}
|
||||
|
||||
// Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily.
|
||||
func createImportTracker(ctx context.Context, program *compiler.Program, sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string], checker *checker.Checker) ImportTracker {
|
||||
allDirectImports := getDirectImportsMap(ctx, program, sourceFiles, checker)
|
||||
return func(exportSymbol *ast.Symbol, exportInfo *ExportInfo, isForRename bool) *ImportsResult {
|
||||
directImports, indirectUsers := getImportersForExport(sourceFiles, sourceFilesSet, allDirectImports, exportInfo, checker)
|
||||
importSearches, singleReferences := getSearchesFromDirectImports(directImports, exportSymbol, exportInfo.exportKind, checker, isForRename)
|
||||
return &ImportsResult{importSearches, singleReferences, indirectUsers}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a map from a module symbol to all import statements that directly reference the module
|
||||
func getDirectImportsMap(ctx context.Context, program *compiler.Program, sourceFiles []*ast.SourceFile, checker *checker.Checker) map[*ast.Symbol][]*ast.Node {
|
||||
result := make(map[*ast.Symbol][]*ast.Node)
|
||||
for _, sourceFile := range sourceFiles {
|
||||
if ctx.Err() != nil {
|
||||
return result
|
||||
}
|
||||
forEachImport(program, sourceFile, func(importDecl *ast.Node, moduleSpecifier *ast.Node) {
|
||||
if moduleSymbol := checker.GetSymbolAtLocation(moduleSpecifier); moduleSymbol != nil {
|
||||
result[moduleSymbol] = append(result[moduleSymbol], importDecl)
|
||||
}
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Calls `action` for each import, re-export, or require() in a file
|
||||
func forEachImport(program *compiler.Program, sourceFile *ast.SourceFile, action func(importStatement *ast.Node, imported *ast.Node)) {
|
||||
var implicitImports []*ast.LiteralLikeNode
|
||||
_, jsxSpecifier := program.GetJSXRuntimeImportSpecifier(sourceFile.Path())
|
||||
if jsxSpecifier != nil {
|
||||
implicitImports = append(implicitImports, jsxSpecifier)
|
||||
}
|
||||
importHelpersSpecifier := program.GetImportHelpersImportSpecifier(sourceFile.Path())
|
||||
if importHelpersSpecifier != nil {
|
||||
implicitImports = append(implicitImports, importHelpersSpecifier)
|
||||
}
|
||||
if sourceFile.ExternalModuleIndicator != nil || len(sourceFile.Imports())+len(implicitImports) != 0 {
|
||||
for _, i := range sourceFile.Imports() {
|
||||
action(ast.ImportFromModuleSpecifier(i), i)
|
||||
}
|
||||
for _, i := range implicitImports {
|
||||
action(ast.ImportFromModuleSpecifier(i), i)
|
||||
}
|
||||
} else {
|
||||
forEachPossibleImportOrExportStatement(sourceFile.AsNode(), func(node *ast.Node) bool {
|
||||
switch node.Kind {
|
||||
case ast.KindExportDeclaration, ast.KindImportDeclaration, ast.KindJSImportDeclaration:
|
||||
if specifier := node.ModuleSpecifier(); specifier != nil && ast.IsStringLiteral(specifier) {
|
||||
action(node, specifier)
|
||||
}
|
||||
case ast.KindImportEqualsDeclaration:
|
||||
if isExternalModuleImportEquals(node) {
|
||||
action(node, node.AsImportEqualsDeclaration().ModuleReference.Expression())
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func forEachPossibleImportOrExportStatement(sourceFileLike *ast.Node, action func(statement *ast.Node) bool) bool {
|
||||
for _, statement := range getStatementsOfSourceFileLike(sourceFileLike) {
|
||||
if action(statement) || isAmbientModuleDeclaration(statement) && forEachPossibleImportOrExportStatement(statement, action) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getSourceFileLikeForImportDeclaration(node *ast.Node) *ast.Node {
|
||||
if ast.IsCallExpression(node) || ast.IsJSDocImportTag(node) {
|
||||
return ast.GetSourceFileOfNode(node).AsNode()
|
||||
}
|
||||
parent := node.Parent
|
||||
if ast.IsSourceFile(parent) {
|
||||
return parent
|
||||
}
|
||||
debug.Assert(ast.IsModuleBlock(parent) && isAmbientModuleDeclaration(parent.Parent))
|
||||
return parent.Parent
|
||||
}
|
||||
|
||||
func isAmbientModuleDeclaration(node *ast.Node) bool {
|
||||
return ast.IsModuleDeclaration(node) && ast.IsStringLiteral(node.Name())
|
||||
}
|
||||
|
||||
func getStatementsOfSourceFileLike(node *ast.Node) []*ast.Node {
|
||||
if ast.IsSourceFile(node) {
|
||||
return node.Statements()
|
||||
}
|
||||
if body := node.Body(); body != nil {
|
||||
return body.Statements()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getImportersForExport(
|
||||
sourceFiles []*ast.SourceFile,
|
||||
sourceFilesSet *collections.Set[string],
|
||||
allDirectImports map[*ast.Symbol][]*ast.Node,
|
||||
exportInfo *ExportInfo,
|
||||
checker *checker.Checker,
|
||||
) ([]*ast.Node, []*ast.SourceFile) {
|
||||
var directImports []*ast.Node
|
||||
var indirectUserDeclarations []*ast.Node
|
||||
markSeenDirectImport := nodeSeenTracker()
|
||||
markSeenIndirectUser := nodeSeenTracker()
|
||||
isAvailableThroughGlobal := isSourceFileWithGlobalExports(exportInfo.exportingModuleSymbol.ValueDeclaration)
|
||||
|
||||
getDirectImports := func(moduleSymbol *ast.Symbol) []*ast.Node {
|
||||
return allDirectImports[moduleSymbol]
|
||||
}
|
||||
|
||||
// Adds a module and all of its transitive dependencies as possible indirect users
|
||||
var addIndirectUser func(*ast.Node, bool)
|
||||
addIndirectUser = func(sourceFileLike *ast.Node, addTransitiveDependencies bool) {
|
||||
// When isAvailableThroughGlobal, getIndirectUsers already returns all source files,
|
||||
// so indirectUserDeclarations is never consulted. Nothing to do here.
|
||||
if isAvailableThroughGlobal {
|
||||
return
|
||||
}
|
||||
if !markSeenIndirectUser(sourceFileLike) {
|
||||
return
|
||||
}
|
||||
indirectUserDeclarations = append(indirectUserDeclarations, sourceFileLike)
|
||||
if !addTransitiveDependencies {
|
||||
return
|
||||
}
|
||||
moduleSymbol := checker.GetMergedSymbol(sourceFileLike.Symbol())
|
||||
if moduleSymbol == nil {
|
||||
return
|
||||
}
|
||||
debug.Assert(moduleSymbol.Flags&ast.SymbolFlagsModule != 0)
|
||||
for _, directImport := range getDirectImports(moduleSymbol) {
|
||||
if !ast.IsImportTypeNode(directImport) {
|
||||
addIndirectUser(getSourceFileLikeForImportDeclaration(directImport), true /*addTransitiveDependencies*/)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isExported := func(node *ast.Node, stopAtAmbientModule bool) bool {
|
||||
for node != nil && !(stopAtAmbientModule && isAmbientModuleDeclaration(node)) {
|
||||
if ast.HasSyntacticModifier(node, ast.ModifierFlagsExport) {
|
||||
return true
|
||||
}
|
||||
node = node.Parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
handleImportCall := func(importCall *ast.Node) {
|
||||
top := ast.FindAncestor(importCall, isAmbientModuleDeclaration)
|
||||
if top == nil {
|
||||
top = ast.GetSourceFileOfNode(importCall).AsNode()
|
||||
}
|
||||
addIndirectUser(top, isExported(importCall, true /*stopAtAmbientModule*/))
|
||||
}
|
||||
|
||||
handleNamespaceImport := func(importDeclaration *ast.Node, name *ast.Node, isReExport bool, alreadyAddedDirect bool) {
|
||||
if exportInfo.exportKind == ExportKindExportEquals {
|
||||
// This is a direct import, not import-as-namespace.
|
||||
if !alreadyAddedDirect {
|
||||
directImports = append(directImports, importDeclaration)
|
||||
}
|
||||
} else if !isAvailableThroughGlobal {
|
||||
sourceFileLike := getSourceFileLikeForImportDeclaration(importDeclaration)
|
||||
debug.Assert(ast.IsSourceFile(sourceFileLike) || ast.IsModuleDeclaration(sourceFileLike))
|
||||
addIndirectUser(sourceFileLike, isReExport || findNamespaceReExports(sourceFileLike, name, checker))
|
||||
}
|
||||
}
|
||||
|
||||
var handleDirectImports func(*ast.Symbol)
|
||||
handleDirectImports = func(exportingModuleSymbol *ast.Symbol) {
|
||||
theseDirectImports := getDirectImports(exportingModuleSymbol)
|
||||
for _, direct := range theseDirectImports {
|
||||
if !markSeenDirectImport(direct) {
|
||||
continue
|
||||
}
|
||||
// !!! cancellation
|
||||
switch direct.Kind {
|
||||
case ast.KindCallExpression:
|
||||
if ast.IsImportCall(direct) {
|
||||
handleImportCall(direct)
|
||||
} else if !isAvailableThroughGlobal {
|
||||
parent := direct.Parent
|
||||
if exportInfo.exportKind == ExportKindExportEquals && ast.IsVariableDeclaration(parent) {
|
||||
name := parent.Name()
|
||||
if ast.IsIdentifier(name) {
|
||||
directImports = append(directImports, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
case ast.KindIdentifier:
|
||||
// Nothing
|
||||
case ast.KindImportEqualsDeclaration:
|
||||
handleNamespaceImport(direct, direct.Name(), ast.HasSyntacticModifier(direct, ast.ModifierFlagsExport), false /*alreadyAddedDirect*/)
|
||||
case ast.KindImportDeclaration, ast.KindJSImportDeclaration, ast.KindJSDocImportTag:
|
||||
directImports = append(directImports, direct)
|
||||
if importClause := direct.ImportClause(); importClause != nil {
|
||||
if namedBindings := importClause.AsImportClause().NamedBindings; namedBindings != nil && ast.IsNamespaceImport(namedBindings) {
|
||||
handleNamespaceImport(direct, namedBindings.Name(), false /*isReExport*/, true /*alreadyAddedDirect*/)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isAvailableThroughGlobal && ast.IsDefaultImport(direct) {
|
||||
addIndirectUser(getSourceFileLikeForImportDeclaration(direct), false)
|
||||
// Add a check for indirect uses to handle synthetic default imports
|
||||
}
|
||||
case ast.KindExportDeclaration:
|
||||
exportClause := direct.AsExportDeclaration().ExportClause
|
||||
if exportClause == nil {
|
||||
// This is `export * from "foo"`, so imports of this module may import the export too.
|
||||
handleDirectImports(getContainingModuleSymbol(direct, checker))
|
||||
} else if ast.IsNamespaceExport(exportClause) {
|
||||
// `export * as foo from "foo"` add to indirect uses
|
||||
addIndirectUser(getSourceFileLikeForImportDeclaration(direct), true /*addTransitiveDependencies*/)
|
||||
} else {
|
||||
// This is `export { foo } from "foo"` and creates an alias symbol, so recursive search will get handle re-exports.
|
||||
directImports = append(directImports, direct)
|
||||
}
|
||||
case ast.KindImportType:
|
||||
// Only check for typeof import('xyz')
|
||||
if !isAvailableThroughGlobal && direct.AsImportTypeNode().IsTypeOf && direct.AsImportTypeNode().Qualifier == nil && isExported(direct, false) {
|
||||
addIndirectUser(ast.GetSourceFileOfNode(direct).AsNode(), true /*addTransitiveDependencies*/)
|
||||
}
|
||||
directImports = append(directImports, direct)
|
||||
default:
|
||||
debug.FailBadSyntaxKind(direct, "Unexpected import kind.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getIndirectUsers := func() []*ast.SourceFile {
|
||||
if isAvailableThroughGlobal {
|
||||
// It has `export as namespace`, so anything could potentially use it.
|
||||
return sourceFiles
|
||||
}
|
||||
// Module augmentations may use this module's exports without importing it.
|
||||
for _, decl := range exportInfo.exportingModuleSymbol.Declarations {
|
||||
if ast.IsExternalModuleAugmentation(decl) && sourceFilesSet.Has(ast.GetSourceFileOfNode(decl).FileName()) {
|
||||
addIndirectUser(decl, false)
|
||||
}
|
||||
}
|
||||
// This may return duplicates (if there are multiple module declarations in a single source file, all importing the same thing as a namespace), but `State.markSearchedSymbol` will handle that.
|
||||
return core.Map(indirectUserDeclarations, ast.GetSourceFileOfNode)
|
||||
}
|
||||
|
||||
handleDirectImports(exportInfo.exportingModuleSymbol)
|
||||
return directImports, getIndirectUsers()
|
||||
}
|
||||
|
||||
func getContainingModuleSymbol(importer *ast.Node, checker *checker.Checker) *ast.Symbol {
|
||||
return checker.GetMergedSymbol(getSourceFileLikeForImportDeclaration(importer).Symbol())
|
||||
}
|
||||
|
||||
// Returns 'true' is the namespace 'name' is re-exported from this module, and 'false' if it is only used locally
|
||||
func findNamespaceReExports(sourceFileLike *ast.Node, name *ast.Node, checker *checker.Checker) bool {
|
||||
namespaceImportSymbol := checker.GetSymbolAtLocation(name)
|
||||
return forEachPossibleImportOrExportStatement(sourceFileLike, func(statement *ast.Node) bool {
|
||||
if !ast.IsExportDeclaration(statement) {
|
||||
return false
|
||||
}
|
||||
exportClause := statement.AsExportDeclaration().ExportClause
|
||||
moduleSpecifier := statement.ModuleSpecifier()
|
||||
return moduleSpecifier == nil && exportClause != nil && ast.IsNamedExports(exportClause) && core.Some(exportClause.Elements(), func(element *ast.Node) bool {
|
||||
return checker.GetExportSpecifierLocalTargetSymbol(element) == namespaceImportSymbol
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func getSearchesFromDirectImports(
|
||||
directImports []*ast.Node,
|
||||
exportSymbol *ast.Symbol,
|
||||
exportKind ExportKind,
|
||||
checker *checker.Checker,
|
||||
isForRename bool,
|
||||
) ([]LocationAndSymbol, []*ast.Node) {
|
||||
var importSearches []LocationAndSymbol
|
||||
var singleReferences []*ast.Node
|
||||
|
||||
addSearch := func(location *ast.Node, symbol *ast.Symbol) {
|
||||
importSearches = append(importSearches, LocationAndSymbol{location, symbol})
|
||||
}
|
||||
|
||||
isNameMatch := func(name string) bool {
|
||||
// Use name of "default" even in `export =` case because we may have allowSyntheticDefaultImports
|
||||
return name == exportSymbol.Name || exportKind != ExportKindNamed && name == ast.InternalSymbolNameDefault
|
||||
}
|
||||
|
||||
// `import x = require("./x")` or `import * as x from "./x"`.
|
||||
// An `export =` may be imported by this syntax, so it may be a direct import.
|
||||
// If it's not a direct import, it will be in `indirectUsers`, so we don't have to do anything here.
|
||||
handleNamespaceImportLike := func(importName *ast.Node) {
|
||||
// Don't rename an import that already has a different name than the export.
|
||||
if exportKind == ExportKindExportEquals && (!isForRename || isNameMatch(importName.Text())) {
|
||||
addSearch(importName, checker.GetSymbolAtLocation(importName))
|
||||
}
|
||||
}
|
||||
|
||||
searchForNamedImport := func(namedBindings *ast.Node) {
|
||||
if namedBindings == nil {
|
||||
return
|
||||
}
|
||||
for _, element := range namedBindings.Elements() {
|
||||
name := element.Name()
|
||||
propertyName := element.PropertyName()
|
||||
if !isNameMatch(core.OrElse(propertyName, name).Text()) {
|
||||
continue
|
||||
}
|
||||
if propertyName != nil {
|
||||
// This is `import { foo as bar } from "./a"` or `export { foo as bar } from "./a"`. `foo` isn't a local in the file, so just add it as a single reference.
|
||||
singleReferences = append(singleReferences, propertyName)
|
||||
// If renaming `{ foo as bar }`, don't touch `bar`, just `foo`.
|
||||
// But do rename `foo` in ` { default as foo }` if that's the original export name.
|
||||
if !isForRename || name.Text() == exportSymbol.Name {
|
||||
// Search locally for `bar`.
|
||||
addSearch(name, checker.GetSymbolAtLocation(name))
|
||||
}
|
||||
} else {
|
||||
var localSymbol *ast.Symbol
|
||||
if ast.IsExportSpecifier(element) && element.PropertyName() != nil {
|
||||
localSymbol = checker.GetExportSpecifierLocalTargetSymbol(element)
|
||||
} else {
|
||||
localSymbol = checker.GetSymbolAtLocation(name)
|
||||
}
|
||||
addSearch(name, localSymbol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleImport := func(decl *ast.Node) {
|
||||
if ast.IsImportEqualsDeclaration(decl) {
|
||||
if isExternalModuleImportEquals(decl) {
|
||||
handleNamespaceImportLike(decl.Name())
|
||||
}
|
||||
return
|
||||
}
|
||||
if ast.IsIdentifier(decl) {
|
||||
handleNamespaceImportLike(decl)
|
||||
return
|
||||
}
|
||||
if ast.IsImportTypeNode(decl) {
|
||||
if qualifier := decl.AsImportTypeNode().Qualifier; qualifier != nil {
|
||||
firstIdentifier := ast.GetFirstIdentifier(qualifier)
|
||||
if firstIdentifier.Text() == ast.SymbolName(exportSymbol) {
|
||||
singleReferences = append(singleReferences, firstIdentifier)
|
||||
}
|
||||
} else if exportKind == ExportKindExportEquals {
|
||||
singleReferences = append(singleReferences, decl.AsImportTypeNode().Argument.AsLiteralTypeNode().Literal)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Ignore if there's a grammar error
|
||||
if !ast.IsStringLiteral(decl.ModuleSpecifier()) {
|
||||
return
|
||||
}
|
||||
if ast.IsExportDeclaration(decl) {
|
||||
if exportClause := decl.AsExportDeclaration().ExportClause; exportClause != nil && ast.IsNamedExports(exportClause) {
|
||||
searchForNamedImport(exportClause)
|
||||
}
|
||||
return
|
||||
}
|
||||
if importClause := decl.ImportClause(); importClause != nil {
|
||||
if namedBindings := importClause.AsImportClause().NamedBindings; namedBindings != nil {
|
||||
switch namedBindings.Kind {
|
||||
case ast.KindNamespaceImport:
|
||||
handleNamespaceImportLike(namedBindings.Name())
|
||||
case ast.KindNamedImports:
|
||||
// 'default' might be accessed as a named import `{ default as foo }`.
|
||||
if exportKind == ExportKindNamed || exportKind == ExportKindDefault {
|
||||
searchForNamedImport(namedBindings)
|
||||
}
|
||||
}
|
||||
}
|
||||
// `export =` might be imported by a default import if `--allowSyntheticDefaultImports` is on, so this handles both ExportKind.Default and ExportKind.ExportEquals.
|
||||
// If a default import has the same name as the default export, allow to rename it.
|
||||
// Given `import f` and `export default function f`, we will rename both, but for `import g` we will rename just that.
|
||||
if name := importClause.Name(); name != nil && (exportKind == ExportKindDefault || exportKind == ExportKindExportEquals) && (!isForRename || name.Text() == symbolNameNoDefault(exportSymbol)) {
|
||||
defaultImportAlias := checker.GetSymbolAtLocation(name)
|
||||
addSearch(name, defaultImportAlias)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, decl := range directImports {
|
||||
handleImport(decl)
|
||||
}
|
||||
return importSearches, singleReferences
|
||||
}
|
||||
|
||||
func getImportOrExportSymbol(node *ast.Node, symbol *ast.Symbol, checker *checker.Checker, comingFromExport bool) *ImportExportSymbol {
|
||||
exportInfo := func(symbol *ast.Symbol, kind ExportKind) *ImportExportSymbol {
|
||||
if exportInfo := getExportInfo(symbol, kind, checker); exportInfo != nil {
|
||||
return &ImportExportSymbol{
|
||||
kind: ImpExpKindExport,
|
||||
symbol: symbol,
|
||||
exportInfo: exportInfo,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
getExport := func() *ImportExportSymbol {
|
||||
getExportAssignmentExport := func(ex *ast.Node) *ImportExportSymbol {
|
||||
// Get the symbol for the `export =` node; its parent is the module it's the export of.
|
||||
if ex.Symbol().Parent == nil {
|
||||
return nil
|
||||
}
|
||||
exportKind := core.IfElse(ex.AsExportAssignment().IsExportEquals, ExportKindExportEquals, ExportKindDefault)
|
||||
return &ImportExportSymbol{
|
||||
kind: ImpExpKindExport,
|
||||
symbol: symbol,
|
||||
exportInfo: &ExportInfo{
|
||||
exportingModuleSymbol: ex.Symbol().Parent,
|
||||
exportKind: exportKind,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Not meant for use with export specifiers or export assignment.
|
||||
getExportKindForDeclaration := func(node *ast.Node) ExportKind {
|
||||
if ast.HasSyntacticModifier(node, ast.ModifierFlagsDefault) {
|
||||
return ExportKindDefault
|
||||
}
|
||||
return ExportKindNamed
|
||||
}
|
||||
|
||||
getSpecialPropertyExport := func(node *ast.Node, useLhsSymbol bool) *ImportExportSymbol {
|
||||
var kind ExportKind
|
||||
switch ast.GetAssignmentDeclarationKind(node) {
|
||||
case ast.JSDeclarationKindExportsProperty:
|
||||
kind = ExportKindNamed
|
||||
case ast.JSDeclarationKindModuleExports:
|
||||
kind = ExportKindExportEquals
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
sym := symbol
|
||||
if useLhsSymbol {
|
||||
sym = node.Symbol()
|
||||
}
|
||||
if sym == nil {
|
||||
return nil
|
||||
}
|
||||
return exportInfo(sym, kind)
|
||||
}
|
||||
|
||||
parent := node.Parent
|
||||
grandparent := parent.Parent
|
||||
if symbol.ExportSymbol != nil {
|
||||
if ast.IsPropertyAccessExpression(parent) {
|
||||
// When accessing an export of a JS module, there's no alias. The symbol will still be flagged as an export even though we're at the use.
|
||||
// So check that we are at the declaration.
|
||||
if ast.IsBinaryExpression(grandparent) && slices.Contains(symbol.Declarations, parent) {
|
||||
return getSpecialPropertyExport(grandparent, false /*useLhsSymbol*/)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return exportInfo(symbol.ExportSymbol, getExportKindForDeclaration(parent))
|
||||
} else {
|
||||
exportNode := getExportNode(parent, node)
|
||||
switch {
|
||||
case exportNode != nil && (ast.HasSyntacticModifier(exportNode, ast.ModifierFlagsExport) || ast.IsImplicitlyExportedJSDocDeclaration(exportNode)):
|
||||
if ast.IsImportEqualsDeclaration(exportNode) && exportNode.AsImportEqualsDeclaration().ModuleReference == node {
|
||||
// We're at `Y` in `export import X = Y`. This is not the exported symbol, the left-hand-side is. So treat this as an import statement.
|
||||
if comingFromExport {
|
||||
return nil
|
||||
}
|
||||
lhsSymbol := checker.GetSymbolAtLocation(exportNode.Name())
|
||||
return &ImportExportSymbol{
|
||||
kind: ImpExpKindImport,
|
||||
symbol: lhsSymbol,
|
||||
}
|
||||
}
|
||||
return exportInfo(symbol, getExportKindForDeclaration(exportNode))
|
||||
case ast.IsNamespaceExport(parent):
|
||||
return exportInfo(symbol, ExportKindNamed)
|
||||
case ast.IsExportAssignment(parent):
|
||||
return getExportAssignmentExport(parent)
|
||||
case ast.IsExportAssignment(grandparent):
|
||||
return getExportAssignmentExport(grandparent)
|
||||
case ast.IsBinaryExpression(parent):
|
||||
return getSpecialPropertyExport(parent, true /*useLhsSymbol*/)
|
||||
case ast.IsBinaryExpression(grandparent):
|
||||
return getSpecialPropertyExport(grandparent, true /*useLhsSymbol*/)
|
||||
case ast.IsJSDocTypedefTag(parent) || ast.IsJSDocCallbackTag(parent):
|
||||
return exportInfo(symbol, ExportKindNamed)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
getImport := func() *ImportExportSymbol {
|
||||
if !isNodeImport(node) {
|
||||
return nil
|
||||
}
|
||||
// JS destructuring from `require(...)` is import-like for references, but the binding element
|
||||
// itself is still a local variable symbol rather than an alias.
|
||||
var importedSymbol *ast.Symbol
|
||||
if symbol.Flags&ast.SymbolFlagsAlias != 0 {
|
||||
importedSymbol = checker.GetImmediateAliasedSymbol(symbol)
|
||||
} else {
|
||||
importedSymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker)
|
||||
}
|
||||
if importedSymbol == nil {
|
||||
return nil
|
||||
}
|
||||
// Search on the local symbol in the exporting module, not the exported symbol.
|
||||
importedSymbol = skipExportSpecifierSymbol(importedSymbol, checker)
|
||||
if importedSymbol == nil {
|
||||
return nil
|
||||
}
|
||||
// Similarly, skip past the symbol for 'export ='
|
||||
if importedSymbol.Name == "export=" {
|
||||
importedSymbol = getExportEqualsLocalSymbol(importedSymbol, checker)
|
||||
if importedSymbol == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// If the import has a different name than the export, do not continue searching.
|
||||
// If `importedName` is undefined, do continue searching as the export is anonymous.
|
||||
// (All imports returned from this function will be ignored anyway if we are in rename and this is a not a named export.)
|
||||
importedName := symbolNameNoDefault(importedSymbol)
|
||||
if importedName == "" || importedName == ast.InternalSymbolNameDefault || importedName == symbol.Name {
|
||||
return &ImportExportSymbol{
|
||||
kind: ImpExpKindImport,
|
||||
symbol: importedSymbol,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
result := getExport()
|
||||
if result == nil && !comingFromExport {
|
||||
result = getImport()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getExportInfo(exportSymbol *ast.Symbol, exportKind ExportKind, c *checker.Checker) *ExportInfo {
|
||||
// Parent can be nil if an `export` is not at the top-level (which is a compile error).
|
||||
if exportSymbol.Parent != nil {
|
||||
exportingModuleSymbol := c.GetMergedSymbol(exportSymbol.Parent)
|
||||
// `export` may appear in a namespace. In that case, just rely on global search.
|
||||
if checker.IsExternalModuleSymbol(exportingModuleSymbol) {
|
||||
return &ExportInfo{
|
||||
exportingModuleSymbol: exportingModuleSymbol,
|
||||
exportKind: exportKind,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a reference is a class expression, the exported node would be its parent.
|
||||
// If a reference is a variable declaration, the exported node would be the variable statement.
|
||||
func getExportNode(parent *ast.Node, node *ast.Node) *ast.Node {
|
||||
var declaration *ast.Node
|
||||
switch {
|
||||
case ast.IsVariableDeclaration(parent):
|
||||
declaration = parent
|
||||
case ast.IsBindingElement(parent):
|
||||
declaration = ast.WalkUpBindingElementsAndPatterns(parent)
|
||||
}
|
||||
if declaration != nil {
|
||||
if parent.Name() == node && !ast.IsCatchClause(declaration.Parent) && ast.IsVariableStatement(declaration.Parent.Parent) {
|
||||
return declaration.Parent.Parent
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return parent
|
||||
}
|
||||
|
||||
func isNodeImport(node *ast.Node) bool {
|
||||
parent := node.Parent
|
||||
switch parent.Kind {
|
||||
case ast.KindImportEqualsDeclaration:
|
||||
return parent.Name() == node && isExternalModuleImportEquals(parent)
|
||||
case ast.KindImportSpecifier:
|
||||
// For a rename import `{ foo as bar }`, don't search for the imported symbol. Just find local uses of `bar`.
|
||||
return parent.PropertyName() == nil
|
||||
case ast.KindImportClause, ast.KindNamespaceImport:
|
||||
debug.Assert(parent.Name() == node)
|
||||
return true
|
||||
case ast.KindBindingElement:
|
||||
return ast.IsInJSFile(node) && ast.IsVariableDeclarationInitializedToBareOrAccessedRequire(parent.Parent.Parent)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isExternalModuleImportEquals(node *ast.Node) bool {
|
||||
moduleReference := node.AsImportEqualsDeclaration().ModuleReference
|
||||
return ast.IsExternalModuleReference(moduleReference) && moduleReference.Expression().Kind == ast.KindStringLiteral
|
||||
}
|
||||
|
||||
// If at an export specifier, go to the symbol it refers to. */
|
||||
func skipExportSpecifierSymbol(symbol *ast.Symbol, checker *checker.Checker) *ast.Symbol {
|
||||
// For `export { foo } from './bar", there's nothing to skip, because it does not create a new alias. But `export { foo } does.
|
||||
for _, declaration := range symbol.Declarations {
|
||||
switch {
|
||||
case ast.IsExportSpecifier(declaration) && declaration.PropertyName() == nil && declaration.Parent.Parent.ModuleSpecifier() == nil:
|
||||
return core.OrElse(checker.GetExportSpecifierLocalTargetSymbol(declaration), symbol)
|
||||
case ast.IsPropertyAccessExpression(declaration) && ast.IsModuleExportsAccessExpression(declaration.Expression()) && !ast.IsPrivateIdentifier(declaration.Name()):
|
||||
// Export of form 'module.exports.propName = expr';
|
||||
return checker.GetSymbolAtLocation(declaration)
|
||||
case ast.IsShorthandPropertyAssignment(declaration) && ast.IsBinaryExpression(declaration.Parent.Parent) && ast.GetAssignmentDeclarationKind(declaration.Parent.Parent) == ast.JSDeclarationKindModuleExports:
|
||||
return checker.GetExportSpecifierLocalTargetSymbol(declaration.Name())
|
||||
}
|
||||
}
|
||||
return symbol
|
||||
}
|
||||
|
||||
func getExportEqualsLocalSymbol(importedSymbol *ast.Symbol, checker *checker.Checker) *ast.Symbol {
|
||||
if importedSymbol.Flags&ast.SymbolFlagsAlias != 0 {
|
||||
return checker.GetImmediateAliasedSymbol(importedSymbol)
|
||||
}
|
||||
decl := importedSymbol.ValueDeclaration
|
||||
debug.Assert(decl != nil)
|
||||
switch {
|
||||
case ast.IsExportAssignment(decl):
|
||||
return decl.Expression().Symbol()
|
||||
case ast.IsBinaryExpression(decl):
|
||||
return decl.AsBinaryExpression().Right.Symbol()
|
||||
case ast.IsSourceFile(decl):
|
||||
return decl.Symbol()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func symbolNameNoDefault(symbol *ast.Symbol) string {
|
||||
if symbol.Name != ast.InternalSymbolNameDefault {
|
||||
return symbol.Name
|
||||
}
|
||||
for _, decl := range symbol.Declarations {
|
||||
name := ast.GetNameOfDeclaration(decl)
|
||||
if name != nil && ast.IsIdentifier(name) {
|
||||
return name.Text()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// findModuleReferences finds all references to a module symbol across the given source files.
|
||||
// This includes import statements, <reference> directives, and implicit references (e.g., JSX runtime imports).
|
||||
func findModuleReferences(program *compiler.Program, sourceFiles []*ast.SourceFile, searchModuleSymbol *ast.Symbol, checker *checker.Checker) []ModuleReference {
|
||||
refs := []ModuleReference{}
|
||||
|
||||
for _, referencingFile := range sourceFiles {
|
||||
searchSourceFile := searchModuleSymbol.ValueDeclaration
|
||||
if searchSourceFile != nil && searchSourceFile.Kind == ast.KindSourceFile {
|
||||
// Check <reference path> directives
|
||||
for _, ref := range referencingFile.ReferencedFiles {
|
||||
if program.GetSourceFileFromReference(referencingFile, ref) == searchSourceFile.AsSourceFile() {
|
||||
refs = append(refs, ModuleReference{
|
||||
kind: ModuleReferenceKindReference,
|
||||
referencingFile: referencingFile,
|
||||
ref: ref,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check <reference types> directives
|
||||
for _, ref := range referencingFile.TypeReferenceDirectives {
|
||||
referenced := program.GetResolvedTypeReferenceDirectiveFromTypeReferenceDirective(ref, referencingFile)
|
||||
if referenced != nil && referenced.ResolvedFileName == searchSourceFile.AsSourceFile().FileName() {
|
||||
refs = append(refs, ModuleReference{
|
||||
kind: ModuleReferenceKindReference,
|
||||
referencingFile: referencingFile,
|
||||
ref: ref,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check all imports (including require() calls)
|
||||
forEachImport(program, referencingFile, func(importDecl *ast.Node, moduleSpecifier *ast.Node) {
|
||||
moduleSymbol := checker.GetSymbolAtLocation(moduleSpecifier)
|
||||
if moduleSymbol == searchModuleSymbol {
|
||||
if ast.NodeIsSynthesized(importDecl) {
|
||||
refs = append(refs, ModuleReference{
|
||||
kind: ModuleReferenceKindImplicit,
|
||||
literal: moduleSpecifier,
|
||||
referencingFile: referencingFile,
|
||||
})
|
||||
} else {
|
||||
refs = append(refs, ModuleReference{
|
||||
kind: ModuleReferenceKindImport,
|
||||
literal: moduleSpecifier,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
||||
927
tools/tsgo/internal/ls/inlay_hints.go
Normal file
927
tools/tsgo/internal/ls/inlay_hints.go
Normal file
@@ -0,0 +1,927 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"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/core"
|
||||
"github.com/microsoft/typescript-go/internal/debug"
|
||||
"github.com/microsoft/typescript-go/internal/evaluator"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/nodebuilder"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
func (l *LanguageService) ProvideInlayHint(
|
||||
ctx context.Context,
|
||||
params *lsproto.InlayHintParams,
|
||||
) (lsproto.InlayHintResponse, error) {
|
||||
userPreferences := l.UserPreferences()
|
||||
inlayHintPreferences := userPreferences.InlayHints
|
||||
if !isAnyInlayHintEnabled(inlayHintPreferences) {
|
||||
return lsproto.InlayHintsOrNull{InlayHints: nil}, nil
|
||||
}
|
||||
|
||||
program, file := l.getProgramAndFile(params.TextDocument.Uri)
|
||||
quotePreference := lsutil.GetQuotePreference(file, userPreferences)
|
||||
|
||||
checker, done := program.GetTypeCheckerForFile(ctx, file)
|
||||
defer done()
|
||||
inlayHintState := &inlayHintState{
|
||||
ctx: ctx,
|
||||
span: l.converters.FromLSPRange(file, params.Range),
|
||||
preferences: inlayHintPreferences,
|
||||
quotePreference: quotePreference,
|
||||
file: file,
|
||||
checker: checker,
|
||||
converters: l.converters,
|
||||
}
|
||||
inlayHintState.visit(file.AsNode())
|
||||
return lsproto.InlayHintsOrNull{InlayHints: &inlayHintState.result}, nil
|
||||
}
|
||||
|
||||
type inlayHintState struct {
|
||||
ctx context.Context
|
||||
span core.TextRange
|
||||
preferences lsutil.InlayHintsPreferences
|
||||
quotePreference lsutil.QuotePreference
|
||||
file *ast.SourceFile
|
||||
checker *checker.Checker
|
||||
converters *lsconv.Converters
|
||||
result []*lsproto.InlayHint
|
||||
}
|
||||
|
||||
func (s *inlayHintState) visit(node *ast.Node) bool {
|
||||
if node == nil || node.End()-node.Pos() == 0 || node.Flags&ast.NodeFlagsReparsed != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch node.Kind {
|
||||
case ast.KindModuleDeclaration, ast.KindClassDeclaration, ast.KindInterfaceDeclaration,
|
||||
ast.KindFunctionDeclaration, ast.KindClassExpression, ast.KindFunctionExpression,
|
||||
ast.KindMethodDeclaration, ast.KindArrowFunction:
|
||||
if s.ctx.Err() != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if !s.span.Intersects(node.Loc) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ast.IsTypeNode(node) && !ast.IsExpressionWithTypeArguments(node) {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.preferences.IncludeInlayVariableTypeHints.IsTrue() && ast.IsVariableDeclaration(node) {
|
||||
s.visitVariableLikeDeclaration(node)
|
||||
} else if s.preferences.IncludeInlayPropertyDeclarationTypeHints.IsTrue() && ast.IsPropertyDeclaration(node) {
|
||||
s.visitVariableLikeDeclaration(node)
|
||||
} else if s.preferences.IncludeInlayEnumMemberValueHints.IsTrue() && ast.IsEnumMember(node) {
|
||||
s.visitEnumMember(node)
|
||||
} else if shouldShowParameterNameHints(s.preferences) && (ast.IsCallExpression(node) || ast.IsNewExpression(node)) {
|
||||
s.visitCallOrNewExpression(node)
|
||||
} else {
|
||||
if s.preferences.IncludeInlayFunctionParameterTypeHints.IsTrue() &&
|
||||
ast.IsFunctionLikeDeclaration(node) &&
|
||||
ast.HasContextSensitiveParameters(node) {
|
||||
s.visitFunctionLikeForParameterType(node)
|
||||
}
|
||||
if s.preferences.IncludeInlayFunctionLikeReturnTypeHints.IsTrue() &&
|
||||
isSignatureSupportingReturnAnnotation(node) {
|
||||
s.visitFunctionDeclarationLikeForReturnType(node)
|
||||
}
|
||||
}
|
||||
return node.ForEachChild(s.visit)
|
||||
}
|
||||
|
||||
// FunctionDeclaration | MethodDeclaration | GetAccessor | FunctionExpression | ArrowFunction
|
||||
func (s *inlayHintState) visitFunctionDeclarationLikeForReturnType(decl *ast.FunctionLikeDeclaration) {
|
||||
if ast.IsArrowFunction(decl) {
|
||||
if astnav.FindChildOfKind(decl, ast.KindOpenParenToken, s.file) == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
typeAnnotation := decl.Type()
|
||||
if typeAnnotation != nil || decl.Body() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
signature := s.checker.GetSignatureFromDeclaration(decl)
|
||||
if signature == nil {
|
||||
return
|
||||
}
|
||||
|
||||
typePredicate := s.checker.GetTypePredicateOfSignature(signature)
|
||||
|
||||
if typePredicate != nil && typePredicate.Type() != nil {
|
||||
hintParts := s.typePredicateToInlayHintParts(typePredicate)
|
||||
s.addTypeHints(hintParts, s.getTypeAnnotationPosition(decl))
|
||||
return
|
||||
}
|
||||
|
||||
returnType := s.checker.GetReturnTypeOfSignature(signature)
|
||||
if isModuleReferenceType(returnType) {
|
||||
return
|
||||
}
|
||||
|
||||
hintParts := s.typeToInlayHintParts(returnType)
|
||||
s.addTypeHints(hintParts, s.getTypeAnnotationPosition(decl))
|
||||
}
|
||||
|
||||
func (s *inlayHintState) visitCallOrNewExpression(expr *ast.CallOrNewExpression) {
|
||||
args := expr.Arguments()
|
||||
if len(args) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
signature := s.checker.GetResolvedSignature(expr)
|
||||
if signature == nil {
|
||||
return
|
||||
}
|
||||
|
||||
signatureParamPos := 0
|
||||
for _, originalArg := range args {
|
||||
arg := ast.SkipParentheses(originalArg)
|
||||
if shouldShowLiteralParameterNameHintsOnly(s.preferences) && !isHintableLiteral(arg) {
|
||||
signatureParamPos++
|
||||
continue
|
||||
}
|
||||
|
||||
spreadArgs := 0
|
||||
if ast.IsSpreadElement(arg) {
|
||||
spreadType := s.checker.GetTypeAtLocation(arg.Expression())
|
||||
if spreadType.IsTupleType() {
|
||||
elementFlags := spreadType.Target().AsTupleType().ElementFlags()
|
||||
fixedLength := spreadType.Target().AsTupleType().FixedLength()
|
||||
if fixedLength == 0 {
|
||||
continue
|
||||
}
|
||||
firstOptionalIndex := slices.IndexFunc(elementFlags, func(f checker.ElementFlags) bool {
|
||||
return f&checker.ElementFlagsRequired == 0
|
||||
})
|
||||
requiredArgs := core.IfElse(firstOptionalIndex < 0, fixedLength, firstOptionalIndex)
|
||||
if requiredArgs > 0 {
|
||||
spreadArgs = requiredArgs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
identifierInfo := s.getParameterIdentifierInfoAtPosition(signature, signatureParamPos)
|
||||
signatureParamPos = signatureParamPos + core.IfElse(spreadArgs > 0, spreadArgs, 1)
|
||||
if identifierInfo == nil {
|
||||
return
|
||||
}
|
||||
|
||||
parameter := identifierInfo.parameter
|
||||
parameterName := identifierInfo.name
|
||||
isFirstVariadicArgument := identifierInfo.isRestParameter
|
||||
parameterNameNotSameAsArgument := s.preferences.IncludeInlayParameterNameHintsWhenArgumentMatchesName.IsTrue() ||
|
||||
!identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName)
|
||||
if !parameterNameNotSameAsArgument && !isFirstVariadicArgument {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.leadingCommentsContainsParameterName(arg, parameterName) {
|
||||
continue
|
||||
}
|
||||
|
||||
s.addParameterHints(
|
||||
parameterName,
|
||||
parameter,
|
||||
astnav.GetStartOfNode(originalArg, s.file, false /*includeJSDoc*/),
|
||||
isFirstVariadicArgument,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *inlayHintState) visitEnumMember(member *ast.EnumMemberNode) {
|
||||
if member.Initializer() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
enumValue := s.checker.GetConstantValue(member)
|
||||
if enumValue != nil {
|
||||
s.addEnumMemberValueHints(evaluator.AnyToString(enumValue), member.End())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *inlayHintState) visitVariableLikeDeclaration(decl *ast.VariableOrPropertyDeclaration) {
|
||||
if decl.Initializer() == nil &&
|
||||
!(ast.IsPropertyDeclaration(decl) && s.checker.GetTypeAtLocation(decl).Flags()&checker.TypeFlagsAny == 0) ||
|
||||
ast.IsBindingPattern(decl.Name()) || (ast.IsVariableDeclaration(decl) && !isHintableDeclaration(decl)) {
|
||||
return
|
||||
}
|
||||
|
||||
typeAnnotation := decl.Type()
|
||||
if typeAnnotation != nil {
|
||||
return
|
||||
}
|
||||
|
||||
declarationType := s.checker.GetTypeAtLocation(decl)
|
||||
if isModuleReferenceType(declarationType) {
|
||||
return
|
||||
}
|
||||
|
||||
hintParts := s.typeToInlayHintParts(declarationType)
|
||||
var hintText string
|
||||
if hintParts.String != nil {
|
||||
hintText = *hintParts.String
|
||||
} else if hintParts.InlayHintLabelParts != nil {
|
||||
var b strings.Builder
|
||||
for _, part := range *hintParts.InlayHintLabelParts {
|
||||
b.WriteString(part.Value)
|
||||
}
|
||||
hintText = b.String()
|
||||
}
|
||||
if !s.preferences.IncludeInlayVariableTypeHintsWhenTypeMatchesName.IsTrue() &&
|
||||
!ast.IsComputedPropertyName(decl.Name()) &&
|
||||
stringutil.EquateStringCaseInsensitive(decl.Name().Text(), hintText) {
|
||||
return
|
||||
}
|
||||
s.addTypeHints(hintParts, decl.Name().End())
|
||||
}
|
||||
|
||||
func (s *inlayHintState) visitFunctionLikeForParameterType(node *ast.FunctionLikeDeclaration) {
|
||||
signature := s.checker.GetSignatureFromDeclaration(node)
|
||||
if signature == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pos := 0
|
||||
for _, param := range node.Parameters() {
|
||||
if isHintableDeclaration(param) {
|
||||
var symbol *ast.Symbol
|
||||
if ast.IsThisParameter(param) {
|
||||
symbol = signature.ThisParameter()
|
||||
} else {
|
||||
symbol = signature.Parameters()[pos]
|
||||
}
|
||||
s.addParameterTypeHint(param, symbol)
|
||||
}
|
||||
if ast.IsThisParameter(param) {
|
||||
continue
|
||||
}
|
||||
pos++
|
||||
}
|
||||
}
|
||||
|
||||
func (s *inlayHintState) addParameterTypeHint(node *ast.ParameterDeclarationNode, symbol *ast.Symbol) {
|
||||
typeAnnotation := node.Type()
|
||||
if typeAnnotation != nil || symbol == nil {
|
||||
return
|
||||
}
|
||||
typeHints := s.getParameterDeclarationTypeHints(symbol)
|
||||
if typeHints == nil {
|
||||
return
|
||||
}
|
||||
var pos int
|
||||
if node.QuestionToken() != nil {
|
||||
pos = node.QuestionToken().End()
|
||||
} else {
|
||||
pos = node.Name().End()
|
||||
}
|
||||
s.addTypeHints(*typeHints, pos)
|
||||
}
|
||||
|
||||
func (s *inlayHintState) getParameterDeclarationTypeHints(symbol *ast.Symbol) *lsproto.StringOrInlayHintLabelParts {
|
||||
valueDeclaration := symbol.ValueDeclaration
|
||||
if valueDeclaration == nil || !ast.IsParameterDeclaration(valueDeclaration) {
|
||||
return nil
|
||||
}
|
||||
|
||||
signatureParamType := s.checker.GetTypeOfSymbolAtLocation(symbol, valueDeclaration)
|
||||
if isModuleReferenceType(signatureParamType) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return new(s.typeToInlayHintParts(signatureParamType))
|
||||
}
|
||||
|
||||
func (s *inlayHintState) typeToInlayHintParts(t *checker.Type) lsproto.StringOrInlayHintLabelParts {
|
||||
flags := nodebuilder.FlagsIgnoreErrors | nodebuilder.FlagsAllowUniqueESSymbolType |
|
||||
nodebuilder.FlagsUseAliasDefinedOutsideCurrentScope
|
||||
idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol)
|
||||
// !!! Avoid type node reuse so we collect identifier symbols.
|
||||
typeNode := s.checker.TypeToTypeNode(t, nil /*enclosingDeclaration*/, flags, idToSymbol)
|
||||
debug.Assert(typeNode != nil, "should always get typenode")
|
||||
return lsproto.StringOrInlayHintLabelParts{
|
||||
InlayHintLabelParts: new(s.getInlayHintLabelParts(typeNode, idToSymbol)),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *inlayHintState) typePredicateToInlayHintParts(typePredicate *checker.TypePredicate) lsproto.StringOrInlayHintLabelParts {
|
||||
flags := nodebuilder.FlagsIgnoreErrors | nodebuilder.FlagsAllowUniqueESSymbolType |
|
||||
nodebuilder.FlagsUseAliasDefinedOutsideCurrentScope
|
||||
idToSymbol := make(map[*ast.IdentifierNode]*ast.Symbol)
|
||||
// !!! Avoid type node reuse so we collect identifier symbols.
|
||||
typeNode := s.checker.TypePredicateToTypePredicateNode(typePredicate, nil /*enclosingDeclaration*/, flags, idToSymbol)
|
||||
debug.Assert(typeNode != nil, "should always get typePredicateNode")
|
||||
return lsproto.StringOrInlayHintLabelParts{
|
||||
InlayHintLabelParts: new(s.getInlayHintLabelParts(typeNode, idToSymbol)),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *inlayHintState) addTypeHints(hint lsproto.StringOrInlayHintLabelParts, position int) {
|
||||
if hint.String != nil {
|
||||
hint.String = new(": " + *hint.String)
|
||||
} else {
|
||||
hint.InlayHintLabelParts = new(append([]*lsproto.InlayHintLabelPart{{Value: ": "}}, *hint.InlayHintLabelParts...))
|
||||
}
|
||||
s.result = append(s.result, &lsproto.InlayHint{
|
||||
Label: hint,
|
||||
Position: s.converters.PositionToLineAndCharacter(s.file, core.TextPos(position)),
|
||||
Kind: new(lsproto.InlayHintKindType),
|
||||
PaddingLeft: new(true),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *inlayHintState) addEnumMemberValueHints(text string, position int) {
|
||||
s.result = append(s.result, &lsproto.InlayHint{
|
||||
Label: lsproto.StringOrInlayHintLabelParts{
|
||||
String: new("= " + text),
|
||||
},
|
||||
Position: s.converters.PositionToLineAndCharacter(s.file, core.TextPos(position)),
|
||||
PaddingLeft: new(true),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *inlayHintState) addParameterHints(text string, parameter *ast.IdentifierNode, position int, isFirstVariadicArgument bool) {
|
||||
hintText := core.IfElse(isFirstVariadicArgument, "...", "") + text
|
||||
displayParts := []*lsproto.InlayHintLabelPart{
|
||||
s.getNodeDisplayPart(hintText, parameter),
|
||||
{
|
||||
Value: ":",
|
||||
},
|
||||
}
|
||||
labelParts := lsproto.StringOrInlayHintLabelParts{InlayHintLabelParts: &displayParts}
|
||||
|
||||
s.result = append(s.result, &lsproto.InlayHint{
|
||||
Label: labelParts,
|
||||
Position: s.converters.PositionToLineAndCharacter(s.file, core.TextPos(position)),
|
||||
Kind: new(lsproto.InlayHintKindParameter),
|
||||
PaddingRight: new(true),
|
||||
})
|
||||
}
|
||||
|
||||
func shouldShowParameterNameHints(preferences lsutil.InlayHintsPreferences) bool {
|
||||
return (preferences.IncludeInlayParameterNameHints == lsutil.IncludeInlayParameterNameHintsLiterals ||
|
||||
preferences.IncludeInlayParameterNameHints == lsutil.IncludeInlayParameterNameHintsAll)
|
||||
}
|
||||
|
||||
func shouldShowLiteralParameterNameHintsOnly(preferences lsutil.InlayHintsPreferences) bool {
|
||||
return preferences.IncludeInlayParameterNameHints == lsutil.IncludeInlayParameterNameHintsLiterals
|
||||
}
|
||||
|
||||
// node is FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration | GetAccessor
|
||||
func isSignatureSupportingReturnAnnotation(node *ast.Node) bool {
|
||||
return ast.IsArrowFunction(node) || ast.IsFunctionExpression(node) || ast.IsFunctionDeclaration(node) ||
|
||||
ast.IsMethodDeclaration(node) || ast.IsGetAccessorDeclaration(node)
|
||||
}
|
||||
|
||||
func isHintableDeclaration(node *ast.VariableOrParameterDeclaration) bool {
|
||||
if (ast.IsPartOfParameterDeclaration(node) || ast.IsVariableDeclaration(node) && ast.IsVarConst(node)) &&
|
||||
node.Initializer() != nil {
|
||||
initializer := ast.SkipParentheses(node.Initializer())
|
||||
return !(isHintableLiteral(initializer) || ast.IsNewExpression(initializer) ||
|
||||
ast.IsObjectLiteralExpression(initializer) || ast.IsAssertionExpression(initializer))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isHintableLiteral(node *ast.Node) bool {
|
||||
switch node.Kind {
|
||||
case ast.KindPrefixUnaryExpression:
|
||||
operand := node.AsPrefixUnaryExpression().Operand
|
||||
return ast.IsLiteralExpression(operand) || ast.IsIdentifier(operand) && ast.IsInfinityOrNaNString(operand.Text())
|
||||
case ast.KindTrueKeyword, ast.KindFalseKeyword, ast.KindNullKeyword,
|
||||
ast.KindNoSubstitutionTemplateLiteral, ast.KindTemplateExpression:
|
||||
return true
|
||||
case ast.KindIdentifier:
|
||||
name := node.Text()
|
||||
return name == "undefined" || ast.IsInfinityOrNaNString(name)
|
||||
}
|
||||
return ast.IsLiteralExpression(node)
|
||||
}
|
||||
|
||||
func isModuleReferenceType(t *checker.Type) bool {
|
||||
symbol := t.Symbol()
|
||||
return symbol != nil && symbol.Flags&ast.SymbolFlagsModule != 0
|
||||
}
|
||||
|
||||
func (s *inlayHintState) getInlayHintLabelParts(node *ast.Node, idToSymbol map[*ast.IdentifierNode]*ast.Symbol) []*lsproto.InlayHintLabelPart {
|
||||
var parts []*lsproto.InlayHintLabelPart
|
||||
|
||||
var visitForDisplayParts func(node *ast.Node)
|
||||
var visitDisplayPartList func(nodes []*ast.Node, separator string)
|
||||
var visitParametersAndTypeParameters func(node *ast.SignatureDeclaration)
|
||||
|
||||
visitForDisplayParts = func(node *ast.Node) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := scanner.TokenToString(node.Kind)
|
||||
if tokenString != "" {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: tokenString})
|
||||
return
|
||||
}
|
||||
|
||||
if ast.IsLiteralExpression(node) {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: s.getLiteralText(node)})
|
||||
return
|
||||
}
|
||||
|
||||
switch node.Kind {
|
||||
case ast.KindIdentifier:
|
||||
identifierText := node.Text()
|
||||
var name *ast.Node
|
||||
if symbol := idToSymbol[node]; symbol != nil && len(symbol.Declarations) != 0 {
|
||||
name = ast.GetNameOfDeclaration(symbol.Declarations[0])
|
||||
}
|
||||
if name != nil {
|
||||
parts = append(parts, s.getNodeDisplayPart(identifierText, name))
|
||||
} else {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: identifierText})
|
||||
}
|
||||
case ast.KindQualifiedName:
|
||||
visitForDisplayParts(node.AsQualifiedName().Left)
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "."})
|
||||
visitForDisplayParts(node.AsQualifiedName().Right)
|
||||
case ast.KindTypePredicate:
|
||||
if node.AsTypePredicateNode().AssertsModifier != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "asserts "})
|
||||
}
|
||||
visitForDisplayParts(node.AsTypePredicateNode().ParameterName)
|
||||
if node.Type() != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " is "})
|
||||
visitForDisplayParts(node.Type())
|
||||
}
|
||||
case ast.KindTypeReference:
|
||||
visitForDisplayParts(node.AsTypeReferenceNode().TypeName)
|
||||
if len(node.TypeArguments()) > 0 {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
|
||||
visitDisplayPartList(node.TypeArguments(), ",")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
|
||||
}
|
||||
case ast.KindTypeParameter:
|
||||
if len(node.ModifierNodes()) > 0 {
|
||||
visitDisplayPartList(node.ModifierNodes(), "")
|
||||
}
|
||||
visitForDisplayParts(node.Name())
|
||||
if node.AsTypeParameterDeclaration().Constraint != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " extends "})
|
||||
visitForDisplayParts(node.AsTypeParameterDeclaration().Constraint)
|
||||
}
|
||||
if node.AsTypeParameterDeclaration().DefaultType != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " = "})
|
||||
visitForDisplayParts(node.AsTypeParameterDeclaration().DefaultType)
|
||||
}
|
||||
case ast.KindParameter:
|
||||
if len(node.ModifierNodes()) > 0 {
|
||||
visitDisplayPartList(node.ModifierNodes(), " ")
|
||||
}
|
||||
if node.AsParameterDeclaration().DotDotDotToken != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "..."})
|
||||
}
|
||||
visitForDisplayParts(node.Name())
|
||||
if node.QuestionToken() != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
|
||||
}
|
||||
if node.Type() != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
|
||||
visitForDisplayParts(node.Type())
|
||||
}
|
||||
case ast.KindConstructorType:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "new "})
|
||||
visitParametersAndTypeParameters(node)
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " => "})
|
||||
visitForDisplayParts(node.Type())
|
||||
case ast.KindTypeQuery:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "typeof "})
|
||||
visitForDisplayParts(node.AsTypeQueryNode().ExprName)
|
||||
if len(node.TypeArguments()) > 0 {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
|
||||
visitDisplayPartList(node.TypeArguments(), ", ")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
|
||||
}
|
||||
case ast.KindTypeLiteral:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "{"})
|
||||
if len(node.Members()) > 0 {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
|
||||
visitDisplayPartList(node.Members(), "; ")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
|
||||
}
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "}"})
|
||||
case ast.KindArrayType:
|
||||
visitForDisplayParts(node.AsArrayTypeNode().ElementType)
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "[]"})
|
||||
case ast.KindTupleType:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
|
||||
visitDisplayPartList(node.Elements(), ", ")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
|
||||
case ast.KindNamedTupleMember:
|
||||
if node.AsNamedTupleMember().DotDotDotToken != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "..."})
|
||||
}
|
||||
visitForDisplayParts(node.Name())
|
||||
if node.QuestionToken() != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
|
||||
}
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
|
||||
visitForDisplayParts(node.Type())
|
||||
case ast.KindOptionalType:
|
||||
visitForDisplayParts(node.Type())
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
|
||||
case ast.KindRestType:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "..."})
|
||||
visitForDisplayParts(node.Type())
|
||||
case ast.KindUnionType:
|
||||
if node.AsUnionTypeNode().Types != nil {
|
||||
visitDisplayPartList(node.AsUnionTypeNode().Types.Nodes, " | ")
|
||||
}
|
||||
case ast.KindIntersectionType:
|
||||
if node.AsIntersectionTypeNode().Types != nil {
|
||||
visitDisplayPartList(node.AsIntersectionTypeNode().Types.Nodes, " & ")
|
||||
}
|
||||
case ast.KindConditionalType:
|
||||
visitForDisplayParts(node.AsConditionalTypeNode().CheckType)
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " extends "})
|
||||
visitForDisplayParts(node.AsConditionalTypeNode().ExtendsType)
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " ? "})
|
||||
visitForDisplayParts(node.AsConditionalTypeNode().TrueType)
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " : "})
|
||||
visitForDisplayParts(node.AsConditionalTypeNode().FalseType)
|
||||
case ast.KindInferType:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "infer "})
|
||||
visitForDisplayParts(node.AsInferTypeNode().TypeParameter)
|
||||
case ast.KindParenthesizedType:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "("})
|
||||
visitForDisplayParts(node.Type())
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ")"})
|
||||
case ast.KindTypeOperator:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: scanner.TokenToString(node.AsTypeOperatorNode().Operator)})
|
||||
visitForDisplayParts(node.Type())
|
||||
case ast.KindIndexedAccessType:
|
||||
visitForDisplayParts(node.AsIndexedAccessTypeNode().ObjectType)
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
|
||||
visitForDisplayParts(node.AsIndexedAccessTypeNode().IndexType)
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
|
||||
case ast.KindMappedType:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "{ "})
|
||||
if node.AsMappedTypeNode().ReadonlyToken != nil {
|
||||
if node.AsMappedTypeNode().ReadonlyToken.Kind == ast.KindPlusToken {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "+"})
|
||||
} else if node.AsMappedTypeNode().ReadonlyToken.Kind == ast.KindMinusToken {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "-"})
|
||||
}
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "readonly "})
|
||||
}
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
|
||||
visitForDisplayParts(node.AsMappedTypeNode().TypeParameter)
|
||||
if node.AsMappedTypeNode().NameType != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " as "})
|
||||
visitForDisplayParts(node.AsMappedTypeNode().NameType)
|
||||
}
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
|
||||
if node.QuestionToken() != nil {
|
||||
if node.QuestionToken().Kind == ast.KindPlusToken {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "+"})
|
||||
} else if node.QuestionToken().Kind == ast.KindMinusToken {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "-"})
|
||||
}
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "?"})
|
||||
}
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
|
||||
if node.Type() != nil {
|
||||
visitForDisplayParts(node.Type())
|
||||
}
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "; }"})
|
||||
case ast.KindLiteralType:
|
||||
visitForDisplayParts(node.AsLiteralTypeNode().Literal)
|
||||
case ast.KindFunctionType:
|
||||
visitParametersAndTypeParameters(node)
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " => "})
|
||||
visitForDisplayParts(node.Type())
|
||||
case ast.KindImportType:
|
||||
if node.AsImportTypeNode().IsTypeOf {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "typeof "})
|
||||
}
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "import("})
|
||||
visitForDisplayParts(node.AsImportTypeNode().Argument)
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ")"})
|
||||
if node.AsImportTypeNode().Qualifier != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "."})
|
||||
visitForDisplayParts(node.AsImportTypeNode().Qualifier)
|
||||
}
|
||||
if len(node.TypeArguments()) > 0 {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
|
||||
visitDisplayPartList(node.TypeArguments(), ", ")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
|
||||
}
|
||||
case ast.KindPropertySignature:
|
||||
if len(node.ModifierNodes()) > 0 {
|
||||
visitDisplayPartList(node.ModifierNodes(), " ")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
|
||||
}
|
||||
visitForDisplayParts(node.Name())
|
||||
if node.PostfixToken() != nil {
|
||||
parts = append(
|
||||
parts,
|
||||
&lsproto.InlayHintLabelPart{
|
||||
Value: scanner.TokenToString(node.PostfixToken().Kind),
|
||||
},
|
||||
)
|
||||
}
|
||||
if node.Type() != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
|
||||
visitForDisplayParts(node.Type())
|
||||
}
|
||||
case ast.KindIndexSignature:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
|
||||
visitDisplayPartList(node.Parameters(), ", ")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
|
||||
if node.Type() != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
|
||||
visitForDisplayParts(node.Type())
|
||||
}
|
||||
case ast.KindMethodSignature:
|
||||
if len(node.ModifierNodes()) > 0 {
|
||||
visitDisplayPartList(node.ModifierNodes(), " ")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
|
||||
}
|
||||
visitForDisplayParts(node.Name())
|
||||
if node.PostfixToken() != nil {
|
||||
parts = append(
|
||||
parts,
|
||||
&lsproto.InlayHintLabelPart{
|
||||
Value: scanner.TokenToString(node.PostfixToken().Kind),
|
||||
},
|
||||
)
|
||||
}
|
||||
visitParametersAndTypeParameters(node)
|
||||
if node.Type() != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
|
||||
visitForDisplayParts(node.Type())
|
||||
}
|
||||
case ast.KindCallSignature:
|
||||
visitParametersAndTypeParameters(node)
|
||||
if node.Type() != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
|
||||
visitForDisplayParts(node.Type())
|
||||
}
|
||||
case ast.KindConstructSignature:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "new "})
|
||||
visitParametersAndTypeParameters(node)
|
||||
if node.Type() != nil {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ": "})
|
||||
visitForDisplayParts(node.Type())
|
||||
}
|
||||
case ast.KindArrayBindingPattern:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
|
||||
visitDisplayPartList(node.Elements(), ", ")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
|
||||
case ast.KindObjectBindingPattern:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "{"})
|
||||
if len(node.Elements()) > 0 {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
|
||||
visitDisplayPartList(node.Elements(), ", ")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: " "})
|
||||
}
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "}"})
|
||||
case ast.KindBindingElement:
|
||||
visitForDisplayParts(node.Name())
|
||||
case ast.KindPrefixUnaryExpression:
|
||||
parts = append(
|
||||
parts,
|
||||
&lsproto.InlayHintLabelPart{
|
||||
Value: scanner.TokenToString(node.AsPrefixUnaryExpression().Operator),
|
||||
},
|
||||
)
|
||||
visitForDisplayParts(node.AsPrefixUnaryExpression().Operand)
|
||||
case ast.KindTemplateLiteralType:
|
||||
visitForDisplayParts(node.AsTemplateLiteralTypeNode().Head)
|
||||
for _, span := range node.AsTemplateLiteralTypeNode().TemplateSpans.Nodes {
|
||||
visitForDisplayParts(span)
|
||||
}
|
||||
case ast.KindTemplateHead:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: s.getLiteralText(node)})
|
||||
case ast.KindTemplateLiteralTypeSpan:
|
||||
visitForDisplayParts(node.Type())
|
||||
visitForDisplayParts(node.AsTemplateLiteralTypeSpan().Literal)
|
||||
case ast.KindTemplateMiddle, ast.KindTemplateTail:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: s.getLiteralText(node)})
|
||||
case ast.KindThisType:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "this"})
|
||||
case ast.KindComputedPropertyName:
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
|
||||
visitForDisplayParts(node.Expression())
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
|
||||
case ast.KindPropertyAccessExpression:
|
||||
visitForDisplayParts(node.Expression())
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "."})
|
||||
visitForDisplayParts(node.Name())
|
||||
case ast.KindElementAccessExpression:
|
||||
visitForDisplayParts(node.Expression())
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "["})
|
||||
visitForDisplayParts(node.AsElementAccessExpression().ArgumentExpression)
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "]"})
|
||||
default:
|
||||
debug.FailBadSyntaxKind(node)
|
||||
}
|
||||
}
|
||||
|
||||
visitDisplayPartList = func(nodes []*ast.Node, separator string) {
|
||||
for i, n := range nodes {
|
||||
if i > 0 {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: separator})
|
||||
}
|
||||
visitForDisplayParts(n)
|
||||
}
|
||||
}
|
||||
|
||||
visitParametersAndTypeParameters = func(node *ast.SignatureDeclaration) {
|
||||
if len(node.TypeParameters()) > 0 {
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "<"})
|
||||
visitDisplayPartList(node.TypeParameters(), ", ")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ">"})
|
||||
}
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: "("})
|
||||
visitDisplayPartList(node.Parameters(), ", ")
|
||||
parts = append(parts, &lsproto.InlayHintLabelPart{Value: ")"})
|
||||
}
|
||||
|
||||
visitForDisplayParts(node)
|
||||
return parts
|
||||
}
|
||||
|
||||
func (s *inlayHintState) getNodeDisplayPart(text string, node *ast.Node) *lsproto.InlayHintLabelPart {
|
||||
file := ast.GetSourceFileOfNode(node)
|
||||
pos := astnav.GetStartOfNode(node, file, false /*includeJSDoc*/)
|
||||
end := node.End()
|
||||
return &lsproto.InlayHintLabelPart{
|
||||
Value: text,
|
||||
Location: &lsproto.Location{
|
||||
Uri: lsconv.FileNameToDocumentURI(file.FileName()),
|
||||
Range: s.converters.ToLSPRange(file, core.NewTextRange(pos, end)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *inlayHintState) getLiteralText(node *ast.LiteralLikeNode) string {
|
||||
switch node.Kind {
|
||||
case ast.KindStringLiteral:
|
||||
if s.quotePreference == lsutil.QuotePreferenceSingle {
|
||||
return `'` + printer.EscapeString(node.Text(), printer.QuoteCharSingleQuote) + `'`
|
||||
}
|
||||
return `"` + printer.EscapeString(node.Text(), printer.QuoteCharDoubleQuote) + `"`
|
||||
case ast.KindTemplateHead, ast.KindTemplateMiddle, ast.KindTemplateTail:
|
||||
rawText := node.RawText()
|
||||
if rawText == "" {
|
||||
rawText = printer.EscapeString(node.Text(), printer.QuoteCharBacktick)
|
||||
}
|
||||
switch node.Kind {
|
||||
case ast.KindTemplateHead:
|
||||
return "`" + rawText + "${"
|
||||
case ast.KindTemplateMiddle:
|
||||
return "}" + rawText + "${"
|
||||
case ast.KindTemplateTail:
|
||||
return "}" + rawText + "`"
|
||||
}
|
||||
}
|
||||
return node.Text()
|
||||
}
|
||||
|
||||
type parameterInfo struct {
|
||||
parameter *ast.IdentifierNode
|
||||
name string
|
||||
isRestParameter bool
|
||||
}
|
||||
|
||||
func (s *inlayHintState) getParameterIdentifierInfoAtPosition(signature *checker.Signature, pos int) *parameterInfo {
|
||||
parameters := signature.Parameters()
|
||||
paramCount := len(parameters) - core.IfElse(signature.HasRestParameter(), 1, 0)
|
||||
if pos < paramCount {
|
||||
param := parameters[pos]
|
||||
paramId := getParameterDeclarationIdentifier(param)
|
||||
if paramId == nil {
|
||||
return nil
|
||||
}
|
||||
return ¶meterInfo{
|
||||
parameter: paramId,
|
||||
name: paramId.Text(),
|
||||
isRestParameter: false,
|
||||
}
|
||||
}
|
||||
|
||||
var restParameter *ast.Symbol
|
||||
var restId *ast.IdentifierNode
|
||||
if paramCount < len(parameters) {
|
||||
restParameter = parameters[paramCount]
|
||||
restId = getParameterDeclarationIdentifier(restParameter)
|
||||
}
|
||||
if restId == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
restType := s.checker.GetTypeOfSymbol(restParameter)
|
||||
if restType.IsTupleType() {
|
||||
associatedNames := make([]*ast.Node, 0, len(restType.Target().AsTupleType().ElementInfos()))
|
||||
for _, elementInfo := range restType.Target().AsTupleType().ElementInfos() {
|
||||
labeledElement := elementInfo.LabeledDeclaration()
|
||||
associatedNames = append(associatedNames, labeledElement)
|
||||
}
|
||||
index := pos - paramCount
|
||||
if index < len(associatedNames) {
|
||||
associatedName := associatedNames[index]
|
||||
if associatedName != nil {
|
||||
debug.Assert(ast.IsIdentifier(associatedName.Name()))
|
||||
var isRestTupleElement bool
|
||||
if ast.IsNamedTupleMember(associatedName) {
|
||||
isRestTupleElement = associatedName.AsNamedTupleMember().DotDotDotToken != nil
|
||||
} else {
|
||||
isRestTupleElement = associatedName.AsParameterDeclaration().DotDotDotToken != nil
|
||||
}
|
||||
return ¶meterInfo{
|
||||
parameter: associatedName.Name(),
|
||||
name: associatedName.Name().Text(),
|
||||
isRestParameter: isRestTupleElement,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if pos == paramCount {
|
||||
return ¶meterInfo{
|
||||
parameter: restId,
|
||||
name: restParameter.Name,
|
||||
isRestParameter: true,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getParameterDeclarationIdentifier(symbol *ast.Symbol) *ast.IdentifierNode {
|
||||
if symbol.ValueDeclaration != nil && ast.IsParameterDeclaration(symbol.ValueDeclaration) && ast.IsIdentifier(symbol.ValueDeclaration.Name()) {
|
||||
return symbol.ValueDeclaration.Name()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func identifierOrAccessExpressionPostfixMatchesParameterName(expr *ast.Expression, parameterName string) bool {
|
||||
if ast.IsIdentifier(expr) {
|
||||
return expr.Text() == parameterName
|
||||
}
|
||||
if ast.IsPropertyAccessExpression(expr) {
|
||||
return expr.Name().Text() == parameterName
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *inlayHintState) leadingCommentsContainsParameterName(node *ast.Node, name string) bool {
|
||||
if !scanner.IsIdentifierText(name, s.file.LanguageVariant) {
|
||||
return false
|
||||
}
|
||||
|
||||
ranges := getLeadingCommentRangesOfNode(node, s.file)
|
||||
fileText := s.file.Text()
|
||||
for r := range ranges {
|
||||
commentText := strings.TrimFunc(fileText[r.Pos():r.End()], func(r rune) bool {
|
||||
return unicode.IsSpace(r) || r == '/' || r == '*'
|
||||
})
|
||||
if commentText == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *inlayHintState) getTypeAnnotationPosition(decl *ast.FunctionLikeDeclaration) int {
|
||||
closeParenToken := astnav.FindChildOfKind(decl, ast.KindCloseParenToken, s.file)
|
||||
if closeParenToken != nil {
|
||||
return closeParenToken.End()
|
||||
}
|
||||
return decl.ParameterList().End()
|
||||
}
|
||||
|
||||
func isAnyInlayHintEnabled(preferences lsutil.InlayHintsPreferences) bool {
|
||||
return preferences.IncludeInlayParameterNameHints != lsutil.IncludeInlayParameterNameHintsNone ||
|
||||
preferences.IncludeInlayFunctionParameterTypeHints.IsTrue() ||
|
||||
preferences.IncludeInlayVariableTypeHints.IsTrue() ||
|
||||
preferences.IncludeInlayPropertyDeclarationTypeHints.IsTrue() ||
|
||||
preferences.IncludeInlayFunctionLikeReturnTypeHints.IsTrue() ||
|
||||
preferences.IncludeInlayEnumMemberValueHints.IsTrue()
|
||||
}
|
||||
161
tools/tsgo/internal/ls/jsdoc.go
Normal file
161
tools/tsgo/internal/ls/jsdoc.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
// JSDocTagInfo mirrors Strada's `JSDocTagInfo`, but renders the tag's text as a
|
||||
// plain string instead of `SymbolDisplayPart[]`.
|
||||
type JSDocTagInfo struct {
|
||||
Name string
|
||||
Text string
|
||||
}
|
||||
|
||||
// GetSymbolDocumentationComment renders a symbol's documentation comment as plain text.
|
||||
// It backs the API's Symbol.getDocumentationComment and mirrors Strada's
|
||||
// getJsDocCommentsFromDeclarations: comments are gathered from each unique declaration,
|
||||
// deduplicated, and joined with line breaks. Like Strada, it does not resolve aliases —
|
||||
// consumers resolve aliases themselves (via getAliasedSymbol) and re-query if desired.
|
||||
func (l *LanguageService) GetSymbolDocumentationComment(c *checker.Checker, symbol *ast.Symbol) string {
|
||||
if symbol == nil {
|
||||
return ""
|
||||
}
|
||||
var parts []string
|
||||
var seen collections.Set[*ast.Node]
|
||||
for _, decl := range symbol.Declarations {
|
||||
if decl == nil {
|
||||
continue
|
||||
}
|
||||
if !seen.AddIfAbsent(decl) {
|
||||
continue
|
||||
}
|
||||
if doc := l.getDocumentationFromDeclaration(c, symbol, decl, decl, lsproto.MarkupKindPlainText, true /*commentOnly*/); doc != "" && !slices.Contains(parts, doc) {
|
||||
parts = append(parts, doc)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// GetSymbolJSDocTags collects a symbol's JSDoc tags. It backs the API's Symbol.getJsDocTags
|
||||
// and mirrors Strada's getJsDocTagsFromDeclarations, except each tag's text is rendered as a
|
||||
// plain string rather than SymbolDisplayPart[]. Tags with no text have an empty Text field.
|
||||
func (l *LanguageService) GetSymbolJSDocTags(symbol *ast.Symbol) []JSDocTagInfo {
|
||||
if symbol == nil {
|
||||
return nil
|
||||
}
|
||||
var infos []JSDocTagInfo
|
||||
var seen collections.Set[*ast.Node]
|
||||
for _, decl := range symbol.Declarations {
|
||||
if decl == nil {
|
||||
continue
|
||||
}
|
||||
if !seen.AddIfAbsent(decl) {
|
||||
continue
|
||||
}
|
||||
tags := declarationJSDocTags(decl)
|
||||
// Skip comments containing @typedef/@callback since they're not associated with a
|
||||
// particular declaration, unless they also carry @param/@return (treated as local docs).
|
||||
hasTypedef := core.Some(tags, func(t *ast.Node) bool {
|
||||
return t.Kind == ast.KindJSDocTypedefTag || t.Kind == ast.KindJSDocCallbackTag
|
||||
})
|
||||
hasParamOrReturn := core.Some(tags, func(t *ast.Node) bool {
|
||||
return t.Kind == ast.KindJSDocParameterTag || t.Kind == ast.KindJSDocReturnTag
|
||||
})
|
||||
if hasTypedef && !hasParamOrReturn {
|
||||
continue
|
||||
}
|
||||
for _, tag := range tags {
|
||||
infos = append(infos, JSDocTagInfo{Name: tag.TagName().Text(), Text: getJSDocTagText(tag)})
|
||||
}
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
// declarationJSDocTags returns the JSDoc tags associated with a declaration, walking the
|
||||
// JSDoc comment location chain like the checker's getAllJSDocTags.
|
||||
func declarationJSDocTags(node *ast.Node) []*ast.Node {
|
||||
if node.Flags&ast.NodeFlagsJSDoc == 0 {
|
||||
for current := node; current != nil; current = ast.GetNextJSDocCommentLocation(current) {
|
||||
jsdocs := current.JSDoc(nil)
|
||||
if len(jsdocs) == 0 {
|
||||
continue
|
||||
}
|
||||
lastJSDoc := jsdocs[len(jsdocs)-1].AsJSDoc()
|
||||
if lastJSDoc.Tags != nil {
|
||||
return lastJSDoc.Tags.Nodes
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getJSDocTagText renders the text of a single JSDoc tag as a plain string, mirroring
|
||||
// Strada's getCommentDisplayParts collapsed from SymbolDisplayPart[] to a string.
|
||||
func getJSDocTagText(tag *ast.Node) string {
|
||||
comment := scanner.GetTextOfJSDocComment(tag.CommentList())
|
||||
addComment := func(s string) string {
|
||||
if comment == "" {
|
||||
return s
|
||||
}
|
||||
return s + " " + comment
|
||||
}
|
||||
switch tag.Kind {
|
||||
case ast.KindJSDocThrowsTag:
|
||||
if te := tag.AsJSDocThrowsTag().TypeExpression; te != nil {
|
||||
return addComment(scanner.GetTextOfNode(te))
|
||||
}
|
||||
return comment
|
||||
case ast.KindJSDocImplementsTag:
|
||||
return addComment(scanner.GetTextOfNode(tag.AsJSDocImplementsTag().ClassName))
|
||||
case ast.KindJSDocAugmentsTag:
|
||||
return addComment(scanner.GetTextOfNode(tag.AsJSDocAugmentsTag().ClassName))
|
||||
case ast.KindJSDocTemplateTag:
|
||||
templateTag := tag.AsJSDocTemplateTag()
|
||||
var b strings.Builder
|
||||
if templateTag.Constraint != nil {
|
||||
b.WriteString(scanner.GetTextOfNode(templateTag.Constraint))
|
||||
}
|
||||
if templateTag.TypeParameters != nil {
|
||||
for i, tp := range templateTag.TypeParameters.Nodes {
|
||||
if i == 0 && b.Len() != 0 {
|
||||
b.WriteString(" ")
|
||||
}
|
||||
if i != 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.WriteString(scanner.GetTextOfNode(tp))
|
||||
}
|
||||
}
|
||||
if comment != "" {
|
||||
if b.Len() != 0 {
|
||||
b.WriteString(" ")
|
||||
}
|
||||
b.WriteString(comment)
|
||||
}
|
||||
return b.String()
|
||||
case ast.KindJSDocTypeTag:
|
||||
return addComment(scanner.GetTextOfNode(tag.AsJSDocTypeTag().TypeExpression))
|
||||
case ast.KindJSDocSatisfiesTag:
|
||||
return addComment(scanner.GetTextOfNode(tag.AsJSDocSatisfiesTag().TypeExpression))
|
||||
case ast.KindJSDocSeeTag:
|
||||
if ne := tag.AsJSDocSeeTag().NameExpression; ne != nil {
|
||||
return addComment(scanner.GetTextOfNode(ne))
|
||||
}
|
||||
return comment
|
||||
case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag:
|
||||
if name := tag.Name(); name != nil {
|
||||
return addComment(scanner.GetTextOfNode(name))
|
||||
}
|
||||
return comment
|
||||
default:
|
||||
return comment
|
||||
}
|
||||
}
|
||||
594
tools/tsgo/internal/ls/jsdoc_snippet.go
Normal file
594
tools/tsgo/internal/ls/jsdoc_snippet.go
Normal file
@@ -0,0 +1,594 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/format"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
type docCommentTemplate struct {
|
||||
newText string
|
||||
}
|
||||
|
||||
type commentOwnerInfo struct {
|
||||
commentOwner *ast.Node
|
||||
parameters []*ast.ParameterDeclarationNode
|
||||
hasReturn bool
|
||||
}
|
||||
|
||||
func (l *LanguageService) getJSDocSnippetCompletion(ctx context.Context, file *ast.SourceFile, position int) *CompletionList {
|
||||
if l.UserPreferences().EnableJSDocCompletions.IsFalse() {
|
||||
return nil
|
||||
}
|
||||
if !isPotentiallyValidJSDocSnippetCompletionPosition(file, position) {
|
||||
return nil
|
||||
}
|
||||
newLine := l.FormatOptions().NewLineCharacter
|
||||
if newLine == "" {
|
||||
newLine = "\n"
|
||||
}
|
||||
template := getDocCommentTemplateAtPosition(file, position, l.UserPreferences().GenerateReturnInDocTemplate.IsTrue(), newLine)
|
||||
if template == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
insertText := template.newText
|
||||
var insertTextFormat *lsproto.InsertTextFormat
|
||||
if clientSupportsItemSnippet(ctx) {
|
||||
insertText = templateToSnippet(insertText, newLine)
|
||||
insertTextFormat = new(lsproto.InsertTextFormatSnippet)
|
||||
}
|
||||
|
||||
editRange := l.getJSDocSnippetCompletionRange(ctx, file, position, insertText)
|
||||
var commitCharacters *[]string
|
||||
if clientSupportsItemCommitCharacters(ctx) {
|
||||
commitCharacters = &[]string{}
|
||||
}
|
||||
item := &CompletionItem{
|
||||
CompletionItem: &lsproto.CompletionItem{
|
||||
Label: "/** */",
|
||||
Kind: new(lsproto.CompletionItemKindText),
|
||||
Detail: new(diagnostics.JSDoc_comment.Localize(locale.FromContext(ctx))),
|
||||
SortText: new("\x00"),
|
||||
InsertTextFormat: insertTextFormat,
|
||||
TextEdit: editRange,
|
||||
CommitCharacters: commitCharacters,
|
||||
},
|
||||
}
|
||||
return &CompletionList{
|
||||
IsIncomplete: false,
|
||||
Items: []*CompletionItem{item},
|
||||
}
|
||||
}
|
||||
|
||||
func isPotentiallyValidJSDocSnippetCompletionPosition(file *ast.SourceFile, position int) bool {
|
||||
text := file.Text()
|
||||
lineStart := format.GetLineStartPositionForPosition(position, file)
|
||||
prefix := text[lineStart:position]
|
||||
if !isJSDocSnippetPrefix(prefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
lineEnd := getLineEndOfPosition(file, position)
|
||||
suffix := text[position:lineEnd]
|
||||
return isJSDocSnippetSuffix(suffix)
|
||||
}
|
||||
|
||||
func (l *LanguageService) getJSDocSnippetCompletionRange(ctx context.Context, file *ast.SourceFile, position int, newText string) *lsproto.TextEditOrInsertReplaceEdit {
|
||||
text := file.Text()
|
||||
lineStart := format.GetLineStartPositionForPosition(position, file)
|
||||
prefix := text[lineStart:position]
|
||||
start := position
|
||||
if prefixStart, ok := getJSDocSnippetPrefixStart(prefix); ok {
|
||||
start = lineStart + prefixStart
|
||||
}
|
||||
|
||||
lineEnd := getLineEndOfPosition(file, position)
|
||||
suffix := text[position:lineEnd]
|
||||
end := position
|
||||
if suffixEnd, ok := getJSDocSnippetSuffixEnd(suffix); ok {
|
||||
end += suffixEnd
|
||||
}
|
||||
|
||||
replacementRange := l.createLspRangeFromBounds(start, end, file)
|
||||
if clientSupportsItemInsertReplace(ctx) {
|
||||
return &lsproto.TextEditOrInsertReplaceEdit{
|
||||
InsertReplaceEdit: &lsproto.InsertReplaceEdit{
|
||||
NewText: newText,
|
||||
Insert: replacementRange,
|
||||
Replace: replacementRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
return &lsproto.TextEditOrInsertReplaceEdit{
|
||||
TextEdit: &lsproto.TextEdit{
|
||||
NewText: newText,
|
||||
Range: replacementRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getDocCommentTemplateAtPosition(sourceFile *ast.SourceFile, position int, generateReturnInDocTemplate bool, newLine string) *docCommentTemplate {
|
||||
tokenAtPos := astnav.GetTokenAtPosition(sourceFile, position)
|
||||
if tokenAtPos == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
existingDocComment := ast.FindAncestor(tokenAtPos, ast.IsJSDoc)
|
||||
docCommentEnd, hasDocCommentAtPosition, hasClosingDocCommentAtPosition := getDocCommentEndAtPosition(sourceFile, position)
|
||||
isInEmptyDocComment := existingDocComment != nil || hasDocCommentAtPosition
|
||||
if isNonEmptyJSDoc(existingDocComment) && hasDocCommentAtPosition && !hasClosingDocCommentAtPosition {
|
||||
reparseText := sourceFile.Text()[:position] + " */" + sourceFile.Text()[position:]
|
||||
reparse := parser.ParseSourceFile(sourceFile.ParseOptions(), reparseText, sourceFile.ScriptKind)
|
||||
return getDocCommentTemplateAtPosition(reparse, position, generateReturnInDocTemplate, newLine)
|
||||
}
|
||||
if isNonEmptyJSDoc(existingDocComment) {
|
||||
return nil
|
||||
}
|
||||
if existingDocComment == nil && hasDocCommentAtPosition {
|
||||
tokenAtPos = astnav.GetTokenAtPosition(sourceFile, skipWhitespace(sourceFile.Text(), docCommentEnd))
|
||||
if tokenAtPos == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
tokenStart := astnav.GetStartOfNode(tokenAtPos, sourceFile, false /*includeJSDoc*/)
|
||||
if !isInEmptyDocComment && tokenStart < position {
|
||||
return nil
|
||||
}
|
||||
|
||||
commentOwnerInfo := getCommentOwnerInfo(tokenAtPos, generateReturnInDocTemplate)
|
||||
if commentOwnerInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
commentOwner := commentOwnerInfo.commentOwner
|
||||
lastJSDoc := core.LastOrNil(commentOwner.JSDoc(sourceFile))
|
||||
if commentOwnerStart := astnav.GetStartOfNode(commentOwner, sourceFile, false /*includeJSDoc*/); commentOwnerStart < position ||
|
||||
lastJSDoc != nil && existingDocComment != nil && lastJSDoc != existingDocComment {
|
||||
return nil
|
||||
}
|
||||
|
||||
indentation := getIndentationStringAtPosition(sourceFile, position)
|
||||
tags := parameterDocComments(commentOwnerInfo.parameters, ast.IsSourceFileJS(sourceFile), indentation, newLine)
|
||||
if commentOwnerInfo.hasReturn {
|
||||
tags += returnsDocComment(indentation, newLine)
|
||||
}
|
||||
|
||||
if tags != "" && !hasJSDocTags(commentOwner, sourceFile) {
|
||||
preamble := "/**" + newLine + indentation + " * "
|
||||
endLine := ""
|
||||
if tokenStart == position {
|
||||
endLine = newLine + indentation
|
||||
}
|
||||
return &docCommentTemplate{newText: preamble + newLine + tags + indentation + " */" + endLine}
|
||||
}
|
||||
return &docCommentTemplate{newText: "/** */"}
|
||||
}
|
||||
|
||||
func getDocCommentEndAtPosition(file *ast.SourceFile, position int) (end int, ok bool, hasClosing bool) {
|
||||
text := file.Text()
|
||||
lineStart := format.GetLineStartPositionForPosition(position, file)
|
||||
lineEnd := getLineEndOfPosition(file, position)
|
||||
prefix := text[lineStart:position]
|
||||
suffix := text[position:lineEnd]
|
||||
if !strings.HasSuffix(trimRightSingleLineWhitespace(prefix), "/**") {
|
||||
return 0, false, false
|
||||
}
|
||||
suffixEnd, hasClosing := getJSDocSnippetSuffixEnd(suffix)
|
||||
return position + suffixEnd, true, hasClosing
|
||||
}
|
||||
|
||||
func skipWhitespace(text string, position int) int {
|
||||
for position < len(text) {
|
||||
ch, size := stringutil.DecodeJSStringRune(text[position:])
|
||||
if size == 0 {
|
||||
break
|
||||
}
|
||||
if !stringutil.IsWhiteSpaceLike(ch) {
|
||||
break
|
||||
}
|
||||
position += size
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
||||
func getCommentOwnerInfo(tokenAtPos *ast.Node, generateReturnInDocTemplate bool) *commentOwnerInfo {
|
||||
for node := tokenAtPos; node != nil; node = node.Parent {
|
||||
info, quit := getCommentOwnerInfoWorker(node, generateReturnInDocTemplate)
|
||||
if info != nil || quit {
|
||||
return info
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCommentOwnerInfoWorker(commentOwner *ast.Node, generateReturnInDocTemplate bool) (*commentOwnerInfo, bool) {
|
||||
if commentOwner == nil {
|
||||
return nil, false
|
||||
}
|
||||
switch commentOwner.Kind {
|
||||
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindMethodDeclaration, ast.KindConstructor, ast.KindMethodSignature, ast.KindArrowFunction:
|
||||
return &commentOwnerInfo{commentOwner: commentOwner, parameters: commentOwner.Parameters(), hasReturn: hasReturn(commentOwner, generateReturnInDocTemplate)}, false
|
||||
case ast.KindPropertyAssignment:
|
||||
return getCommentOwnerInfoWorker(commentOwner.AsPropertyAssignment().Initializer, generateReturnInDocTemplate)
|
||||
case ast.KindClassDeclaration, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration, ast.KindEnumMember, ast.KindTypeAliasDeclaration:
|
||||
return &commentOwnerInfo{commentOwner: commentOwner}, false
|
||||
case ast.KindPropertySignature:
|
||||
if typeNode := commentOwner.AsPropertySignatureDeclaration().Type; typeNode != nil && ast.IsFunctionTypeNode(typeNode) {
|
||||
return &commentOwnerInfo{commentOwner: commentOwner, parameters: typeNode.Parameters(), hasReturn: hasReturn(typeNode, generateReturnInDocTemplate)}, false
|
||||
}
|
||||
return &commentOwnerInfo{commentOwner: commentOwner}, false
|
||||
case ast.KindVariableStatement:
|
||||
declarations := commentOwner.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes
|
||||
if len(declarations) == 1 {
|
||||
if initializer := declarations[0].AsVariableDeclaration().Initializer; initializer != nil {
|
||||
if host := getRightHandSideOfAssignment(initializer); host != nil {
|
||||
return &commentOwnerInfo{commentOwner: commentOwner, parameters: host.Parameters(), hasReturn: hasReturn(host, generateReturnInDocTemplate)}, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return &commentOwnerInfo{commentOwner: commentOwner}, false
|
||||
case ast.KindSourceFile:
|
||||
return nil, true
|
||||
case ast.KindModuleDeclaration:
|
||||
if commentOwner.Parent.Kind == ast.KindModuleDeclaration {
|
||||
return nil, false
|
||||
}
|
||||
return &commentOwnerInfo{commentOwner: commentOwner}, false
|
||||
case ast.KindExpressionStatement:
|
||||
return getCommentOwnerInfoWorker(commentOwner.AsExpressionStatement().Expression, generateReturnInDocTemplate)
|
||||
case ast.KindBinaryExpression:
|
||||
binaryExpression := commentOwner.AsBinaryExpression()
|
||||
if ast.GetAssignmentDeclarationKind(commentOwner) == ast.JSDeclarationKindNone {
|
||||
return nil, true
|
||||
}
|
||||
if ast.IsFunctionLike(binaryExpression.Right) {
|
||||
return &commentOwnerInfo{commentOwner: commentOwner, parameters: binaryExpression.Right.Parameters(), hasReturn: hasReturn(binaryExpression.Right, generateReturnInDocTemplate)}, false
|
||||
}
|
||||
return &commentOwnerInfo{commentOwner: commentOwner}, false
|
||||
case ast.KindPropertyDeclaration:
|
||||
if initializer := commentOwner.AsPropertyDeclaration().Initializer; initializer != nil && ast.IsFunctionExpressionOrArrowFunction(initializer) {
|
||||
return &commentOwnerInfo{commentOwner: commentOwner, parameters: initializer.Parameters(), hasReturn: hasReturn(initializer, generateReturnInDocTemplate)}, false
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func hasReturn(node *ast.Node, generateReturnInDocTemplate bool) bool {
|
||||
if !generateReturnInDocTemplate {
|
||||
return false
|
||||
}
|
||||
if ast.IsFunctionTypeNode(node) {
|
||||
return true
|
||||
}
|
||||
if ast.IsArrowFunction(node) {
|
||||
if body := node.Body(); body != nil && ast.IsExpression(body) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return ast.IsFunctionLikeDeclaration(node) && node.Body() != nil && ast.IsBlock(node.Body()) && ast.ForEachReturnStatement(node.Body(), func(*ast.Node) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func getRightHandSideOfAssignment(rightHandSide *ast.Node) *ast.Node {
|
||||
if rightHandSide == nil {
|
||||
return nil
|
||||
}
|
||||
for rightHandSide.Kind == ast.KindParenthesizedExpression {
|
||||
rightHandSide = rightHandSide.AsParenthesizedExpression().Expression
|
||||
}
|
||||
switch rightHandSide.Kind {
|
||||
case ast.KindFunctionExpression, ast.KindArrowFunction:
|
||||
return rightHandSide
|
||||
case ast.KindClassExpression:
|
||||
return core.Find(rightHandSide.Members(), ast.IsConstructorDeclaration)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func parameterDocComments(parameters []*ast.ParameterDeclarationNode, isJavaScriptFile bool, indentation, newLine string) string {
|
||||
var b strings.Builder
|
||||
for i, parameter := range parameters {
|
||||
paramName := fmt.Sprintf("param%d", i)
|
||||
if ast.IsIdentifier(parameter.Name()) {
|
||||
paramName = parameter.Name().Text()
|
||||
}
|
||||
paramType := ""
|
||||
if isJavaScriptFile {
|
||||
if parameter.AsParameterDeclaration().DotDotDotToken != nil {
|
||||
paramType = "{...any} "
|
||||
} else {
|
||||
paramType = "{any} "
|
||||
}
|
||||
}
|
||||
b.WriteString(indentation)
|
||||
b.WriteString(" * @param ")
|
||||
b.WriteString(paramType)
|
||||
b.WriteString(paramName)
|
||||
b.WriteString(newLine)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func returnsDocComment(indentation, newLine string) string {
|
||||
return indentation + " * @returns" + newLine
|
||||
}
|
||||
|
||||
func getIndentationStringAtPosition(sourceFile *ast.SourceFile, position int) string {
|
||||
text := sourceFile.Text()
|
||||
lineStart := format.GetLineStartPositionForPosition(position, sourceFile)
|
||||
pos := lineStart
|
||||
for pos < position {
|
||||
ch, size := stringutil.DecodeJSStringRune(text[pos:])
|
||||
if size == 0 {
|
||||
break
|
||||
}
|
||||
if !stringutil.IsWhiteSpaceSingleLine(ch) {
|
||||
break
|
||||
}
|
||||
pos += size
|
||||
}
|
||||
return text[lineStart:pos]
|
||||
}
|
||||
|
||||
func isNonEmptyJSDoc(jsdoc *ast.Node) bool {
|
||||
if jsdoc == nil {
|
||||
return false
|
||||
}
|
||||
data := jsdoc.AsJSDoc()
|
||||
return data.Comment != nil && len(data.Comment.Nodes) > 0 || data.Tags != nil && len(data.Tags.Nodes) > 0
|
||||
}
|
||||
|
||||
func hasJSDocTags(node *ast.Node, file *ast.SourceFile) bool {
|
||||
jsdocs := node.JSDoc(file)
|
||||
if len(jsdocs) == 0 {
|
||||
return false
|
||||
}
|
||||
tags := jsdocs[len(jsdocs)-1].AsJSDoc().Tags
|
||||
return tags != nil && len(tags.Nodes) > 0
|
||||
}
|
||||
|
||||
func templateToSnippet(template string, newLine string) string {
|
||||
if template == "/** */" {
|
||||
return "/**" + newLine + " * $0" + newLine + " */"
|
||||
}
|
||||
|
||||
snippetIndex := 1
|
||||
template = escapeSnippetText(template)
|
||||
template = stripJSDocTemplateIndentation(template, newLine)
|
||||
return transformJSDocTemplateLines(template, newLine, &snippetIndex)
|
||||
}
|
||||
|
||||
func stripJSDocTemplateIndentation(template string, newLine string) string {
|
||||
lines := strings.Split(template, newLine)
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimLeft(line, " \t")
|
||||
if strings.HasPrefix(trimmed, "/") {
|
||||
lines[i] = trimmed
|
||||
} else if strings.HasPrefix(trimmed, "*") {
|
||||
lines[i] = " " + trimmed
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, newLine)
|
||||
}
|
||||
|
||||
func transformJSDocTemplateLines(template string, newLine string, snippetIndex *int) string {
|
||||
lines := strings.Split(template, newLine)
|
||||
for i, line := range lines {
|
||||
if i > 0 && strings.HasPrefix(lines[i-1], "/**") && lineHasOnlyJSDocAsterisk(line) {
|
||||
lines[i] = line + "$0"
|
||||
continue
|
||||
}
|
||||
if transformed, ok := transformJSDocParamLine(line, snippetIndex); ok {
|
||||
lines[i] = transformed
|
||||
continue
|
||||
}
|
||||
if transformed, ok := transformJSDocReturnsLine(line, snippetIndex); ok {
|
||||
lines[i] = transformed
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, newLine)
|
||||
}
|
||||
|
||||
func lineHasOnlyJSDocAsterisk(line string) bool {
|
||||
line = strings.TrimLeft(line, " \t")
|
||||
return strings.HasPrefix(line, "*") && isOnlySpacesOrTabs(line[1:])
|
||||
}
|
||||
|
||||
func transformJSDocParamLine(line string, snippetIndex *int) (string, bool) {
|
||||
prefix := ""
|
||||
rest := line
|
||||
if strings.HasPrefix(rest, " ") {
|
||||
prefix = " "
|
||||
rest = rest[1:]
|
||||
}
|
||||
if !strings.HasPrefix(rest, "* @param") {
|
||||
return "", false
|
||||
}
|
||||
rest = rest[len("* @param"):]
|
||||
if !startsWithSingleLineWhitespace(rest) {
|
||||
return "", false
|
||||
}
|
||||
rest = strings.TrimLeft(rest, " \t")
|
||||
|
||||
var typeText string
|
||||
if strings.HasPrefix(rest, "{") {
|
||||
closeBrace := strings.IndexByte(rest, '}')
|
||||
if closeBrace < 0 {
|
||||
return "", false
|
||||
}
|
||||
typeText = " " + rest[:closeBrace+1]
|
||||
rest = rest[closeBrace+1:]
|
||||
if !startsWithSingleLineWhitespace(rest) {
|
||||
return "", false
|
||||
}
|
||||
rest = strings.TrimLeft(rest, " \t")
|
||||
}
|
||||
|
||||
paramName, rest, ok := scanNonWhitespace(rest)
|
||||
if !ok || !isOnlySpacesOrTabs(rest) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
out := prefix + "* @param "
|
||||
if typeText == " {any}" || typeText == " {*}" {
|
||||
out += fmt.Sprintf("{${%d:*}} ", *snippetIndex)
|
||||
*snippetIndex++
|
||||
} else if typeText != "" {
|
||||
out += typeText + " "
|
||||
}
|
||||
out += fmt.Sprintf("%s ${%d}", paramName, *snippetIndex)
|
||||
*snippetIndex++
|
||||
return out, true
|
||||
}
|
||||
|
||||
func transformJSDocReturnsLine(line string, snippetIndex *int) (string, bool) {
|
||||
prefix := ""
|
||||
rest := line
|
||||
if strings.HasPrefix(rest, " ") {
|
||||
prefix = " "
|
||||
rest = rest[1:]
|
||||
}
|
||||
if !strings.HasPrefix(rest, "* @returns") || !isOnlySpacesOrTabs(rest[len("* @returns"):]) {
|
||||
return "", false
|
||||
}
|
||||
text := fmt.Sprintf("%s* @returns ${%d}", prefix, *snippetIndex)
|
||||
*snippetIndex++
|
||||
return text, true
|
||||
}
|
||||
|
||||
func scanNonWhitespace(text string) (word string, rest string, ok bool) {
|
||||
if text == "" {
|
||||
return "", "", false
|
||||
}
|
||||
for i := 0; i < len(text); {
|
||||
ch, size := stringutil.DecodeJSStringRune(text[i:])
|
||||
if size == 0 || stringutil.IsWhiteSpaceLike(ch) {
|
||||
if i == 0 {
|
||||
return "", "", false
|
||||
}
|
||||
return text[:i], text[i:], true
|
||||
}
|
||||
i += size
|
||||
}
|
||||
return text, "", true
|
||||
}
|
||||
|
||||
func isJSDocSnippetPrefix(prefix string) bool {
|
||||
trimmed := trimRightSingleLineWhitespace(prefix)
|
||||
if strings.HasSuffix(trimmed, "/**") {
|
||||
return true
|
||||
}
|
||||
start := skipSingleLineWhitespace(prefix, 0)
|
||||
if start >= len(trimmed) || trimmed[start] != '/' {
|
||||
return false
|
||||
}
|
||||
if start+3 > len(trimmed) {
|
||||
return false
|
||||
}
|
||||
for i := start + 1; i < len(trimmed); i++ {
|
||||
if trimmed[i] != '*' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(trimmed)-start >= 3
|
||||
}
|
||||
|
||||
func getJSDocSnippetPrefixStart(prefix string) (int, bool) {
|
||||
trimmed := trimRightSingleLineWhitespace(prefix)
|
||||
for i := len(trimmed) - 1; i >= 0 && trimmed[i] == '*'; i-- {
|
||||
if i > 0 && trimmed[i-1] == '/' {
|
||||
return i - 1, true
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(trimmed, "/") {
|
||||
return len(trimmed) - 1, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func isJSDocSnippetSuffix(suffix string) bool {
|
||||
trimmed := trimRightSingleLineWhitespace(suffix[skipSingleLineWhitespace(suffix, 0):])
|
||||
if trimmed == "" {
|
||||
return true
|
||||
}
|
||||
if !strings.HasSuffix(trimmed, "/") {
|
||||
return false
|
||||
}
|
||||
for i := range len(trimmed) - 1 {
|
||||
if trimmed[i] != '*' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getJSDocSnippetSuffixEnd(suffix string) (int, bool) {
|
||||
pos := skipSingleLineWhitespace(suffix, 0)
|
||||
for pos < len(suffix) && suffix[pos] == '*' {
|
||||
pos++
|
||||
}
|
||||
if pos < len(suffix) && suffix[pos] == '/' {
|
||||
return pos + 1, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func trimRightSingleLineWhitespace(text string) string {
|
||||
end := 0
|
||||
for pos := 0; pos < len(text); {
|
||||
ch, size := stringutil.DecodeJSStringRune(text[pos:])
|
||||
if size == 0 {
|
||||
break
|
||||
}
|
||||
pos += size
|
||||
if !stringutil.IsWhiteSpaceSingleLine(ch) {
|
||||
end = pos
|
||||
}
|
||||
}
|
||||
return text[:end]
|
||||
}
|
||||
|
||||
func skipSingleLineWhitespace(text string, pos int) int {
|
||||
for pos < len(text) {
|
||||
ch, size := stringutil.DecodeJSStringRune(text[pos:])
|
||||
if size == 0 || !stringutil.IsWhiteSpaceSingleLine(ch) {
|
||||
break
|
||||
}
|
||||
pos += size
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
func isOnlySingleLineWhitespace(text string) bool {
|
||||
return skipSingleLineWhitespace(text, 0) == len(text)
|
||||
}
|
||||
|
||||
func startsWithSingleLineWhitespace(text string) bool {
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
ch, size := stringutil.DecodeJSStringRune(text)
|
||||
return size != 0 && stringutil.IsWhiteSpaceSingleLine(ch)
|
||||
}
|
||||
|
||||
func isOnlySpacesOrTabs(text string) bool {
|
||||
for i := range len(text) {
|
||||
if text[i] != ' ' && text[i] != '\t' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
132
tools/tsgo/internal/ls/languageservice.go
Normal file
132
tools/tsgo/internal/ls/languageservice.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/ls/autoimport"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/sourcemap"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
|
||||
)
|
||||
|
||||
type LanguageService struct {
|
||||
projectPath tspath.Path
|
||||
host Host
|
||||
activeConfig lsutil.UserPreferences
|
||||
program *compiler.Program
|
||||
converters *lsconv.Converters
|
||||
documentPositionMappers map[string]*sourcemap.DocumentPositionMapper
|
||||
}
|
||||
|
||||
func NewLanguageService(
|
||||
projectPath tspath.Path,
|
||||
program *compiler.Program,
|
||||
host Host,
|
||||
activeFile string,
|
||||
) *LanguageService {
|
||||
return &LanguageService{
|
||||
projectPath: projectPath,
|
||||
host: host,
|
||||
program: program,
|
||||
converters: host.Converters(),
|
||||
activeConfig: host.GetPreferences(activeFile),
|
||||
documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LanguageService) toPath(fileName string) tspath.Path {
|
||||
return tspath.ToPath(fileName, l.program.GetCurrentDirectory(), l.UseCaseSensitiveFileNames())
|
||||
}
|
||||
|
||||
func (l *LanguageService) GetProgram() *compiler.Program {
|
||||
return l.program
|
||||
}
|
||||
|
||||
func (l *LanguageService) UserPreferences() lsutil.UserPreferences {
|
||||
return l.activeConfig
|
||||
}
|
||||
|
||||
func (l *LanguageService) FormatOptions() lsutil.FormatCodeSettings {
|
||||
return l.activeConfig.FormatCodeSettings
|
||||
}
|
||||
|
||||
func (l *LanguageService) tryGetProgramAndFile(fileName string) (*compiler.Program, *ast.SourceFile) {
|
||||
program := l.GetProgram()
|
||||
file := program.GetSourceFile(fileName)
|
||||
return program, file
|
||||
}
|
||||
|
||||
func (l *LanguageService) getProgramAndFile(documentURI lsproto.DocumentUri) (*compiler.Program, *ast.SourceFile) {
|
||||
fileName := documentURI.FileName()
|
||||
program, file := l.tryGetProgramAndFile(fileName)
|
||||
if file == nil {
|
||||
panic("file not found: " + fileName)
|
||||
}
|
||||
return program, file
|
||||
}
|
||||
|
||||
func (l *LanguageService) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper {
|
||||
d, ok := l.documentPositionMappers[fileName]
|
||||
if !ok {
|
||||
d = sourcemap.GetDocumentPositionMapper(l, fileName)
|
||||
l.documentPositionMappers[fileName] = d
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (l *LanguageService) ReadFile(fileName string) (string, bool) {
|
||||
return l.host.ReadFile(fileName)
|
||||
}
|
||||
|
||||
func (l *LanguageService) UseCaseSensitiveFileNames() bool {
|
||||
return l.host.UseCaseSensitiveFileNames()
|
||||
}
|
||||
|
||||
func (l *LanguageService) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo {
|
||||
return l.host.GetECMALineInfo(fileName)
|
||||
}
|
||||
|
||||
// getPreparedAutoImportView returns an auto-import view for the given file if the registry is prepared
|
||||
// to provide up-to-date auto-imports for it. If not, it returns ErrNeedsAutoImports.
|
||||
// If auto-imports are disabled via user preferences, it returns (nil, nil).
|
||||
func (l *LanguageService) getPreparedAutoImportView(fromFile *ast.SourceFile) (*autoimport.View, error) {
|
||||
if l.UserPreferences().IncludeCompletionsForModuleExports.IsFalse() {
|
||||
return nil, nil
|
||||
}
|
||||
registry := l.host.AutoImportRegistry()
|
||||
if !registry.IsPreparedForImportingFile(fromFile.FileName(), l.projectPath, l.UserPreferences()) {
|
||||
return nil, ErrNeedsAutoImports
|
||||
}
|
||||
|
||||
view := autoimport.NewView(registry, fromFile, l.projectPath, l.program, l.UserPreferences().ModuleSpecifierPreferences())
|
||||
return view, nil
|
||||
}
|
||||
|
||||
// getCurrentAutoImportView returns an auto-import view for the given file, based on the current state
|
||||
// of the auto-import registry, which may or may not be up-to-date.
|
||||
func (l *LanguageService) getCurrentAutoImportView(fromFile *ast.SourceFile) *autoimport.View {
|
||||
return autoimport.NewView(
|
||||
l.host.AutoImportRegistry(),
|
||||
fromFile,
|
||||
l.projectPath,
|
||||
l.program,
|
||||
l.UserPreferences().ModuleSpecifierPreferences(),
|
||||
)
|
||||
}
|
||||
|
||||
// Used for module specifier completions.
|
||||
func (l *LanguageService) DirectoryExists(path string) bool {
|
||||
return l.host.DirectoryExists(path)
|
||||
}
|
||||
|
||||
// Used for module specifier completions.
|
||||
func (l *LanguageService) ReadDirectory(path string, extensions []string, includes []string) []string {
|
||||
return l.host.ReadDirectory(l.program.GetCurrentDirectory(), path, extensions, nil /*excludes*/, includes, vfsmatch.UnlimitedDepth)
|
||||
}
|
||||
|
||||
func (l *LanguageService) GetDirectories(path string) []string {
|
||||
return l.host.GetDirectories(path)
|
||||
}
|
||||
107
tools/tsgo/internal/ls/linkedediting.go
Normal file
107
tools/tsgo/internal/ls/linkedediting.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/debug"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
// allow the client to match more than valid tag names. This allows linked editing when typing is in progress or tag name is incomplete
|
||||
var jsxTagWordPattern = new("[a-zA-Z0-9:\\-\\._$]*")
|
||||
|
||||
func (l *LanguageService) ProvideLinkedEditingRange(ctx context.Context, params *lsproto.LinkedEditingRangeParams) (lsproto.LinkedEditingRangeResponse, error) {
|
||||
_, sourceFile := l.getProgramAndFile(params.TextDocument.Uri)
|
||||
position := l.converters.LineAndCharacterToPosition(sourceFile, params.Position)
|
||||
token := astnav.FindPrecedingToken(sourceFile, int(position))
|
||||
|
||||
if token == nil || token.Parent.Kind == ast.KindSourceFile {
|
||||
return lsproto.LinkedEditingRangeResponse{}, nil
|
||||
}
|
||||
|
||||
if ast.IsJsxFragment(token.Parent.Parent) {
|
||||
fragment := token.Parent.Parent.AsJsxFragment()
|
||||
openFragment := fragment.OpeningFragment
|
||||
closeFragment := fragment.ClosingFragment
|
||||
if openFragment.Flags&ast.NodeFlagsThisNodeOrAnySubNodesHasError != 0 || closeFragment.Flags&ast.NodeFlagsThisNodeOrAnySubNodesHasError != 0 {
|
||||
return lsproto.LinkedEditingRangeResponse{}, nil
|
||||
}
|
||||
|
||||
openPos := core.TextPos(astnav.GetStartOfNode(openFragment.AsNode(), sourceFile, false) + len("<"))
|
||||
closePos := core.TextPos(astnav.GetStartOfNode(closeFragment.AsNode(), sourceFile, false) + len("</"))
|
||||
|
||||
// only allows linked editing right after opening bracket: <| ></| >
|
||||
if (position != openPos) && (position != closePos) {
|
||||
return lsproto.LinkedEditingRangeResponse{}, nil
|
||||
}
|
||||
|
||||
openLineChar := l.converters.PositionToLineAndCharacter(sourceFile, openPos)
|
||||
closeLineChar := l.converters.PositionToLineAndCharacter(sourceFile, closePos)
|
||||
return lsproto.LinkedEditingRangeResponse{
|
||||
LinkedEditingRanges: &lsproto.LinkedEditingRanges{
|
||||
Ranges: []lsproto.Range{
|
||||
{Start: openLineChar, End: openLineChar}, // only return start position for opening tag since the length of a fragment is always 3 and it is unlikely user will type in the middle of a fragment tag
|
||||
{Start: closeLineChar, End: closeLineChar},
|
||||
},
|
||||
WordPattern: jsxTagWordPattern,
|
||||
},
|
||||
}, nil
|
||||
} else {
|
||||
// determines if the cursor is in an element tag
|
||||
tag := ast.FindAncestor(token.Parent, func(n *ast.Node) bool {
|
||||
if ast.IsJsxOpeningElement(n) || ast.IsJsxClosingElement(n) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if tag == nil {
|
||||
return lsproto.LinkedEditingRangeResponse{}, nil
|
||||
}
|
||||
debug.Assert(ast.IsJsxOpeningElement(tag) || ast.IsJsxClosingElement(tag), "tag should be opening or closing element")
|
||||
|
||||
jsxElement := tag.Parent.AsJsxElement()
|
||||
openTag := jsxElement.OpeningElement
|
||||
closeTag := jsxElement.ClosingElement
|
||||
|
||||
openTagNameStart := astnav.GetStartOfNode(openTag.TagName().AsNode(), sourceFile, false)
|
||||
openTagNameEnd := openTag.TagName().End()
|
||||
closeTagNameStart := astnav.GetStartOfNode(closeTag.TagName().AsNode(), sourceFile, false)
|
||||
closeTagNameEnd := closeTag.TagName().End()
|
||||
// do not return linked cursors if tags are not well-formed
|
||||
if openTagNameStart == astnav.GetStartOfNode(openTag.AsNode(), sourceFile, false) || closeTagNameStart == astnav.GetStartOfNode(closeTag.AsNode(), sourceFile, false) ||
|
||||
openTagNameEnd == openTag.End() || closeTagNameEnd == closeTag.End() {
|
||||
return lsproto.LinkedEditingRangeResponse{}, nil
|
||||
}
|
||||
// only return linked cursors if the cursor is within a tag name
|
||||
positionInt := int(position)
|
||||
if !(openTagNameStart <= positionInt && positionInt <= openTagNameEnd || closeTagNameStart <= positionInt && positionInt <= closeTagNameEnd) {
|
||||
return lsproto.LinkedEditingRangeResponse{}, nil
|
||||
}
|
||||
|
||||
// only return linked cursors if text in both tags is identical
|
||||
openingTagText := scanner.GetTextOfNode(openTag.TagName().AsNode())
|
||||
if openingTagText != scanner.GetTextOfNode(closeTag.TagName().AsNode()) {
|
||||
return lsproto.LinkedEditingRangeResponse{}, nil
|
||||
}
|
||||
|
||||
return lsproto.LinkedEditingRangeResponse{
|
||||
LinkedEditingRanges: &lsproto.LinkedEditingRanges{
|
||||
Ranges: []lsproto.Range{
|
||||
{
|
||||
Start: l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(openTagNameStart)),
|
||||
End: l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(openTagNameEnd)),
|
||||
},
|
||||
{
|
||||
Start: l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(closeTagNameStart)),
|
||||
End: l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(closeTagNameEnd)),
|
||||
},
|
||||
},
|
||||
WordPattern: jsxTagWordPattern,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
356
tools/tsgo/internal/ls/lsconv/converters.go
Normal file
356
tools/tsgo/internal/ls/lsconv/converters.go
Normal file
@@ -0,0 +1,356 @@
|
||||
package lsconv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/bundled"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/diagnosticwriter"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
type Converters struct {
|
||||
getLineMap func(fileName string) *LSPLineMap
|
||||
positionEncoding lsproto.PositionEncodingKind
|
||||
}
|
||||
|
||||
type Script interface {
|
||||
FileName() string
|
||||
Text() string
|
||||
}
|
||||
|
||||
func NewConverters(positionEncoding lsproto.PositionEncodingKind, getLineMap func(fileName string) *LSPLineMap) *Converters {
|
||||
return &Converters{
|
||||
getLineMap: getLineMap,
|
||||
positionEncoding: positionEncoding,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Converters) ToLSPRange(script Script, textRange core.TextRange) lsproto.Range {
|
||||
return lsproto.Range{
|
||||
Start: c.PositionToLineAndCharacter(script, core.TextPos(textRange.Pos())),
|
||||
End: c.PositionToLineAndCharacter(script, core.TextPos(textRange.End())),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Converters) FromLSPRange(script Script, textRange lsproto.Range) core.TextRange {
|
||||
return core.NewTextRange(
|
||||
int(c.LineAndCharacterToPosition(script, textRange.Start)),
|
||||
int(c.LineAndCharacterToPosition(script, textRange.End)),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Converters) FromLSPTextChange(script Script, change *lsproto.TextDocumentContentChangePartial) core.TextChange {
|
||||
return core.TextChange{
|
||||
TextRange: c.FromLSPRange(script, change.Range),
|
||||
NewText: change.Text,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Converters) ToLSPLocation(script Script, rng core.TextRange) lsproto.Location {
|
||||
return lsproto.Location{
|
||||
Uri: FileNameToDocumentURI(script.FileName()),
|
||||
Range: c.ToLSPRange(script, rng),
|
||||
}
|
||||
}
|
||||
|
||||
func LanguageKindToScriptKind(languageID lsproto.LanguageKind) core.ScriptKind {
|
||||
switch languageID {
|
||||
case "typescript":
|
||||
return core.ScriptKindTS
|
||||
case "typescriptreact":
|
||||
return core.ScriptKindTSX
|
||||
case "javascript":
|
||||
return core.ScriptKindJS
|
||||
case "javascriptreact":
|
||||
return core.ScriptKindJSX
|
||||
case "json":
|
||||
return core.ScriptKindJSON
|
||||
default:
|
||||
return core.ScriptKindUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/microsoft/vscode-uri/blob/edfdccd976efaf4bb8fdeca87e97c47257721729/src/uri.ts#L455
|
||||
var extraEscapeReplacer = strings.NewReplacer(
|
||||
":", "%3A",
|
||||
"/", "%2F",
|
||||
"?", "%3F",
|
||||
"#", "%23",
|
||||
"[", "%5B",
|
||||
"]", "%5D",
|
||||
"@", "%40",
|
||||
|
||||
"!", "%21",
|
||||
"$", "%24",
|
||||
"&", "%26",
|
||||
"'", "%27",
|
||||
"(", "%28",
|
||||
")", "%29",
|
||||
"*", "%2A",
|
||||
"+", "%2B",
|
||||
",", "%2C",
|
||||
";", "%3B",
|
||||
"=", "%3D",
|
||||
|
||||
" ", "%20",
|
||||
)
|
||||
|
||||
func FileNameToDocumentURI(fileName string) lsproto.DocumentUri {
|
||||
if bundled.IsBundled(fileName) {
|
||||
return lsproto.DocumentUri(fileName)
|
||||
}
|
||||
if tspath.IsDynamicFileName(fileName) {
|
||||
scheme, rest, ok := strings.Cut(fileName[2:], "/")
|
||||
if !ok {
|
||||
panic("invalid file name: " + fileName)
|
||||
}
|
||||
authority, path, ok := strings.Cut(rest, "/")
|
||||
if !ok {
|
||||
panic("invalid file name: " + fileName)
|
||||
}
|
||||
if authority == "ts-nul-authority" {
|
||||
return lsproto.DocumentUri(scheme + ":" + path)
|
||||
}
|
||||
return lsproto.DocumentUri(scheme + "://" + authority + "/" + path)
|
||||
}
|
||||
|
||||
volume, fileName, _ := tspath.SplitVolumePath(fileName)
|
||||
if volume != "" {
|
||||
volume = "/" + extraEscapeReplacer.Replace(volume)
|
||||
}
|
||||
|
||||
fileName = strings.TrimPrefix(fileName, "//")
|
||||
|
||||
parts := strings.Split(fileName, "/")
|
||||
for i, part := range parts {
|
||||
parts[i] = extraEscapeReplacer.Replace(url.PathEscape(part))
|
||||
}
|
||||
|
||||
return lsproto.DocumentUri("file://" + volume + strings.Join(parts, "/"))
|
||||
}
|
||||
|
||||
func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter lsproto.Position) core.TextPos {
|
||||
// UTF-8/16 0-indexed line and character to UTF-8 offset
|
||||
|
||||
lineMap := c.getLineMap(script.FileName())
|
||||
|
||||
line := core.TextPos(lineAndCharacter.Line)
|
||||
char := core.TextPos(lineAndCharacter.Character)
|
||||
|
||||
textLen := core.TextPos(len(script.Text()))
|
||||
|
||||
// Clamp line to valid range.
|
||||
if int(line) >= len(lineMap.LineStarts) {
|
||||
return textLen
|
||||
}
|
||||
|
||||
start := lineMap.LineStarts[line]
|
||||
|
||||
// Determine the end of this line (start of next line, or end of text).
|
||||
var lineEnd core.TextPos
|
||||
if int(line)+1 < len(lineMap.LineStarts) {
|
||||
lineEnd = lineMap.LineStarts[int(line)+1]
|
||||
} else {
|
||||
lineEnd = textLen
|
||||
}
|
||||
|
||||
if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 {
|
||||
return max(start, min(start+char, lineEnd))
|
||||
}
|
||||
|
||||
// Scan from line start counting UTF-16 code units to find the byte position.
|
||||
// Uses DecodeRuneInString (not range + RuneLen) so that invalid UTF-8 bytes
|
||||
// advance by their actual size (1) rather than RuneLen(RuneError) == 3.
|
||||
// This matches the approach in scanner.ComputePositionOfLineAndUTF16Character.
|
||||
var utf16Char core.TextPos
|
||||
pos := int(start)
|
||||
end := int(lineEnd)
|
||||
text := script.Text()
|
||||
for pos < end {
|
||||
r, size := utf8.DecodeRuneInString(text[pos:])
|
||||
u16Len := core.TextPos(utf16.RuneLen(r))
|
||||
if utf16Char+u16Len > char {
|
||||
break
|
||||
}
|
||||
utf16Char += u16Len
|
||||
pos += size
|
||||
}
|
||||
|
||||
return core.TextPos(pos)
|
||||
}
|
||||
|
||||
func (c *Converters) PositionToLineAndCharacter(script Script, position core.TextPos) lsproto.Position {
|
||||
// UTF-8 offset to UTF-8/16 0-indexed line and character
|
||||
|
||||
position = max(0, min(position, core.TextPos(len(script.Text()))))
|
||||
|
||||
lineMap := c.getLineMap(script.FileName())
|
||||
|
||||
line, isLineStart := slices.BinarySearch(lineMap.LineStarts, position)
|
||||
if !isLineStart {
|
||||
line--
|
||||
}
|
||||
line = max(0, min(line, len(lineMap.LineStarts)-1))
|
||||
|
||||
// The current line ranges from lineMap.LineStarts[line] (or 0) to lineMap.LineStarts[line+1] (or len(text)).
|
||||
|
||||
start := lineMap.LineStarts[line]
|
||||
|
||||
var character core.TextPos
|
||||
if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 {
|
||||
character = position - start
|
||||
} else {
|
||||
// We need to rescan the text as UTF-16 to find the character offset.
|
||||
for _, r := range script.Text()[start:position] {
|
||||
character += core.TextPos(utf16.RuneLen(r))
|
||||
}
|
||||
}
|
||||
|
||||
return lsproto.Position{
|
||||
Line: uint32(line),
|
||||
Character: uint32(character),
|
||||
}
|
||||
}
|
||||
|
||||
type diagnosticOptions struct {
|
||||
reportStyleChecksAsWarnings bool
|
||||
relatedInformation bool
|
||||
tagValueSet []lsproto.DiagnosticTag
|
||||
visualStudio bool
|
||||
}
|
||||
|
||||
// DiagnosticToLSPPull converts a diagnostic for pull diagnostics (textDocument/diagnostic)
|
||||
func DiagnosticToLSPPull(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic, reportStyleChecksAsWarnings bool) *lsproto.Diagnostic {
|
||||
clientCaps := lsproto.GetClientCapabilities(ctx)
|
||||
clientDiagnosticCaps := clientCaps.TextDocument.Diagnostic
|
||||
return diagnosticToLSP(ctx, converters, diagnostic, diagnosticOptions{
|
||||
reportStyleChecksAsWarnings: reportStyleChecksAsWarnings, // !!! get through context UserPreferences
|
||||
relatedInformation: clientDiagnosticCaps.RelatedInformation,
|
||||
tagValueSet: clientDiagnosticCaps.TagSupport.ValueSet,
|
||||
visualStudio: clientCaps.VSSupportsVisualStudioExtensions,
|
||||
})
|
||||
}
|
||||
|
||||
// DiagnosticToLSPPush converts a diagnostic for push diagnostics (textDocument/publishDiagnostics)
|
||||
func DiagnosticToLSPPush(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
|
||||
clientCaps := lsproto.GetClientCapabilities(ctx)
|
||||
clientDiagnosticCaps := clientCaps.TextDocument.PublishDiagnostics
|
||||
return diagnosticToLSP(ctx, converters, diagnostic, diagnosticOptions{
|
||||
relatedInformation: clientDiagnosticCaps.RelatedInformation,
|
||||
tagValueSet: clientDiagnosticCaps.TagSupport.ValueSet,
|
||||
visualStudio: clientCaps.VSSupportsVisualStudioExtensions,
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/microsoft/vscode/blob/93e08afe0469712706ca4e268f778cfadf1a43ef/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts#L40C7-L40C29
|
||||
var styleCheckDiagnostics = collections.NewSetFromItems(
|
||||
diagnostics.X_0_is_declared_but_never_used.Code(),
|
||||
diagnostics.X_0_is_declared_but_its_value_is_never_read.Code(),
|
||||
diagnostics.Property_0_is_declared_but_its_value_is_never_read.Code(),
|
||||
diagnostics.All_imports_in_import_declaration_are_unused.Code(),
|
||||
diagnostics.Unreachable_code_detected.Code(),
|
||||
diagnostics.Unused_label.Code(),
|
||||
diagnostics.Fallthrough_case_in_switch.Code(),
|
||||
diagnostics.Not_all_code_paths_return_a_value.Code(),
|
||||
)
|
||||
|
||||
func diagnosticToLSP(ctx context.Context, converters *Converters, diagnostic *ast.Diagnostic, opts diagnosticOptions) *lsproto.Diagnostic {
|
||||
locale := locale.FromContext(ctx)
|
||||
var severity lsproto.DiagnosticSeverity
|
||||
switch diagnostic.Category() {
|
||||
case diagnostics.CategorySuggestion:
|
||||
severity = lsproto.DiagnosticSeverityHint
|
||||
case diagnostics.CategoryMessage:
|
||||
severity = lsproto.DiagnosticSeverityInformation
|
||||
case diagnostics.CategoryWarning:
|
||||
severity = lsproto.DiagnosticSeverityWarning
|
||||
default:
|
||||
severity = lsproto.DiagnosticSeverityError
|
||||
}
|
||||
|
||||
if opts.reportStyleChecksAsWarnings && severity == lsproto.DiagnosticSeverityError && styleCheckDiagnostics.Has(diagnostic.Code()) {
|
||||
severity = lsproto.DiagnosticSeverityWarning
|
||||
}
|
||||
|
||||
var relatedInformation []*lsproto.DiagnosticRelatedInformation
|
||||
if opts.relatedInformation {
|
||||
relatedInformation = make([]*lsproto.DiagnosticRelatedInformation, 0, len(diagnostic.RelatedInformation()))
|
||||
for _, related := range diagnostic.RelatedInformation() {
|
||||
relatedInformation = append(relatedInformation, &lsproto.DiagnosticRelatedInformation{
|
||||
Location: lsproto.Location{
|
||||
Uri: FileNameToDocumentURI(related.File().FileName()),
|
||||
Range: converters.ToLSPRange(related.File(), related.Loc()),
|
||||
},
|
||||
Message: related.Localize(locale),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var tags []lsproto.DiagnosticTag
|
||||
if len(opts.tagValueSet) > 0 && (diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated()) {
|
||||
tags = make([]lsproto.DiagnosticTag, 0, 2)
|
||||
if diagnostic.ReportsUnnecessary() && slices.Contains(opts.tagValueSet, lsproto.DiagnosticTagUnnecessary) {
|
||||
tags = append(tags, lsproto.DiagnosticTagUnnecessary)
|
||||
}
|
||||
if diagnostic.ReportsDeprecated() && slices.Contains(opts.tagValueSet, lsproto.DiagnosticTagDeprecated) {
|
||||
tags = append(tags, lsproto.DiagnosticTagDeprecated)
|
||||
}
|
||||
}
|
||||
|
||||
// For diagnostics without a file (e.g., program diagnostics), use a zero range
|
||||
var lspRange lsproto.Range
|
||||
if diagnostic.File() != nil {
|
||||
lspRange = converters.ToLSPRange(diagnostic.File(), diagnostic.Loc())
|
||||
}
|
||||
|
||||
var code *lsproto.IntegerOrString
|
||||
var source *string
|
||||
if opts.visualStudio {
|
||||
code = &lsproto.IntegerOrString{
|
||||
String: new(fmt.Sprintf("TS%d", diagnostic.Code())),
|
||||
}
|
||||
} else {
|
||||
code = &lsproto.IntegerOrString{
|
||||
Integer: new(diagnostic.Code()),
|
||||
}
|
||||
source = new("ts")
|
||||
}
|
||||
|
||||
return &lsproto.Diagnostic{
|
||||
Range: lspRange,
|
||||
Code: code,
|
||||
Severity: &severity,
|
||||
Message: lsproto.StringOrMarkupContent{String: new(messageChainToString(diagnostic, locale))},
|
||||
Source: source,
|
||||
RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation),
|
||||
Tags: ptrToSliceIfNonEmpty(tags),
|
||||
}
|
||||
}
|
||||
|
||||
func messageChainToString(diagnostic *ast.Diagnostic, locale locale.Locale) string {
|
||||
if len(diagnostic.MessageChain()) == 0 {
|
||||
return diagnostic.Localize(locale)
|
||||
}
|
||||
var b strings.Builder
|
||||
diagnosticwriter.WriteFlattenedASTDiagnosticMessage(&b, diagnostic, "\n", locale)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func ptrToSliceIfNonEmpty[T any](s []T) *[]T {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
329
tools/tsgo/internal/ls/lsconv/converters_test.go
Normal file
329
tools/tsgo/internal/ls/lsconv/converters_test.go
Normal file
@@ -0,0 +1,329 @@
|
||||
package lsconv_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestDocumentURIToFileName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
uri lsproto.DocumentUri
|
||||
fileName string
|
||||
}{
|
||||
{"file:///path/to/file.ts", "/path/to/file.ts"},
|
||||
{"file://server/share/file.ts", "//server/share/file.ts"},
|
||||
{"file:///d%3A/work/tsgo932/lib/utils.ts", "d:/work/tsgo932/lib/utils.ts"},
|
||||
{"file:///D%3A/work/tsgo932/lib/utils.ts", "d:/work/tsgo932/lib/utils.ts"},
|
||||
{"file:///d%3A/work/tsgo932/app/%28test%29/comp/comp-test.tsx", "d:/work/tsgo932/app/(test)/comp/comp-test.tsx"},
|
||||
{"file:///path/to/file.ts#section", "/path/to/file.ts"},
|
||||
{"file:///c:/test/me", "c:/test/me"},
|
||||
{"file://shares/files/c%23/p.cs", "//shares/files/c#/p.cs"},
|
||||
{"file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins/c%23/plugin.json", "c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json"},
|
||||
{"file:///c:/test %25/path", "c:/test %/path"},
|
||||
// {"file:?q", "/"},
|
||||
{"file:///_:/path", "/_:/path"},
|
||||
{"file:///users/me/c%23-projects/", "/users/me/c#-projects/"},
|
||||
{"file://localhost/c%24/GitDevelopment/express", "//localhost/c$/GitDevelopment/express"},
|
||||
{"file:///c%3A/test%20with%20%2525/c%23code", "c:/test with %25/c#code"},
|
||||
|
||||
{"untitled:Untitled-1", "^/untitled/ts-nul-authority/Untitled-1"},
|
||||
{"untitled:Untitled-1#fragment", "^/untitled/ts-nul-authority/Untitled-1#fragment"},
|
||||
{"untitled:c:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt"},
|
||||
{"untitled:C:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/C:/Users/jrieken/Code/abc.txt"},
|
||||
{"untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts", "^/untitled/wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(string(test.uri), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, test.uri.FileName(), test.fileName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileNameToDocumentURI(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
fileName string
|
||||
uri lsproto.DocumentUri
|
||||
}{
|
||||
{"/path/to/file.ts", "file:///path/to/file.ts"},
|
||||
{"//server/share/file.ts", "file://server/share/file.ts"},
|
||||
{"d:/work/tsgo932/lib/utils.ts", "file:///d%3A/work/tsgo932/lib/utils.ts"},
|
||||
{"d:/work/tsgo932/lib/utils.ts", "file:///d%3A/work/tsgo932/lib/utils.ts"},
|
||||
{"d:/work/tsgo932/app/(test)/comp/comp-test.tsx", "file:///d%3A/work/tsgo932/app/%28test%29/comp/comp-test.tsx"},
|
||||
{"/path/to/file.ts", "file:///path/to/file.ts"},
|
||||
{"c:/test/me", "file:///c%3A/test/me"},
|
||||
{"//shares/files/c#/p.cs", "file://shares/files/c%23/p.cs"},
|
||||
{"c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json", "file:///c%3A/Source/Z%C3%BCrich%20or%20Zurich%20%28%CB%88zj%CA%8A%C9%99r%C9%AAk%2C/Code/resources/app/plugins/c%23/plugin.json"},
|
||||
{"c:/test %/path", "file:///c%3A/test%20%25/path"},
|
||||
{"/", "file:///"},
|
||||
{"/_:/path", "file:///_%3A/path"},
|
||||
{"/users/me/c#-projects/", "file:///users/me/c%23-projects/"},
|
||||
{"//localhost/c$/GitDevelopment/express", "file://localhost/c%24/GitDevelopment/express"},
|
||||
{"c:/test with %25/c#code", "file:///c%3A/test%20with%20%2525/c%23code"},
|
||||
|
||||
{"^/untitled/ts-nul-authority/Untitled-1", "untitled:Untitled-1"},
|
||||
{"^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt", "untitled:c:/Users/jrieken/Code/abc.txt"},
|
||||
{"^/untitled/ts-nul-authority///wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts", "untitled://wsl%2Bubuntu/home/jabaile/work/TypeScript-go/newfile.ts"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.fileName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, lsconv.FileNameToDocumentURI(test.fileName), test.uri)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testScript struct {
|
||||
name string
|
||||
text string
|
||||
}
|
||||
|
||||
func (s *testScript) FileName() string { return s.name }
|
||||
func (s *testScript) Text() string { return s.text }
|
||||
|
||||
func newTestConverters(text string) (*lsconv.Converters, *testScript) {
|
||||
script := &testScript{name: "test.ts", text: text}
|
||||
lineMap := lsconv.ComputeLSPLineStarts(text)
|
||||
conv := lsconv.NewConverters(lsproto.PositionEncodingKindUTF16, func(_ string) *lsconv.LSPLineMap {
|
||||
return lineMap
|
||||
})
|
||||
return conv, script
|
||||
}
|
||||
|
||||
// TestConvertersInvalidUTF8 verifies behavior on text containing invalid UTF-8
|
||||
// sequences (e.g. lone continuation bytes). Node's TextDecoder substitutes such
|
||||
// bytes with U+FFFD, so the JS-reference test cannot cover this; we assert the
|
||||
// expected Go-side behavior directly. Each invalid byte advances the byte
|
||||
// position by 1 and the UTF-16 character by 1 (RuneError = 1 code unit).
|
||||
func TestConvertersInvalidUTF8(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Text with invalid UTF-8 byte 0x80 (continuation byte without start byte).
|
||||
// Old code used utf8.RuneLen(RuneError)==3, overshooting the byte offset.
|
||||
text := "a\x80b\ncd"
|
||||
conv, script := newTestConverters(text)
|
||||
|
||||
// (line, char) → byte position. Each row asserts both directions where the
|
||||
// position lies on a character boundary.
|
||||
mappings := []struct {
|
||||
line, char uint32
|
||||
bytePos core.TextPos
|
||||
}{
|
||||
{0, 0, 0}, // 'a'
|
||||
{0, 1, 1}, // invalid byte 0x80
|
||||
{0, 2, 2}, // 'b'
|
||||
{0, 3, 3}, // newline (line end)
|
||||
{1, 0, 4}, // 'c'
|
||||
{1, 1, 5}, // 'd'
|
||||
{1, 2, 6}, // EOF
|
||||
}
|
||||
for _, m := range mappings {
|
||||
lc := lsproto.Position{Line: m.line, Character: m.char}
|
||||
assert.Equal(t, conv.LineAndCharacterToPosition(script, lc), m.bytePos,
|
||||
fmt.Sprintf("LineAndCharacterToPosition(%d,%d)", m.line, m.char))
|
||||
assert.Equal(t, conv.PositionToLineAndCharacter(script, m.bytePos), lc,
|
||||
fmt.Sprintf("PositionToLineAndCharacter(%d)", m.bytePos))
|
||||
}
|
||||
|
||||
// Byte-by-byte round-trip across the entire text.
|
||||
for bytePos := core.TextPos(0); bytePos <= core.TextPos(len(text)); bytePos++ {
|
||||
lc := conv.PositionToLineAndCharacter(script, bytePos)
|
||||
rt := conv.LineAndCharacterToPosition(script, lc)
|
||||
assert.Equal(t, rt, bytePos, fmt.Sprintf("round-trip byte %d", bytePos))
|
||||
}
|
||||
}
|
||||
|
||||
// jsReferenceScript is a Node.js script that, given a list of UTF-8 byte buffers,
|
||||
// computes the authoritative mapping between (line, character in UTF-16 code units)
|
||||
// and UTF-8 byte offsets.
|
||||
//
|
||||
// To avoid any string round-tripping at the protocol boundary, the inputs are sent
|
||||
// as raw bytes: the test writes a length-prefixed binary stream to stdin
|
||||
// ([uint32 little-endian count][uint32 LE len][bytes]...[uint32 LE len][bytes]).
|
||||
// Node reads the buffers and decodes each with TextDecoder('utf-8') — which is
|
||||
// essentially what tsserver / sys.ts does when reading file contents from disk
|
||||
// (read as Buffer, decode as UTF-8 to a JS string with real UTF-16 semantics).
|
||||
//
|
||||
// For each input buffer, Node walks the underlying UTF-8 bytes (NOT the decoded
|
||||
// string) to identify codepoint boundaries: every byte is the start of a codepoint
|
||||
// unless it's a UTF-8 continuation byte (0b10xxxxxx). At each boundary it records
|
||||
// the UTF-8 byte offset and the corresponding UTF-16 code unit offset (in the
|
||||
// decoded JS string) and (line, char) using the LSP line-break rules
|
||||
// (\n, \r, \r\n only).
|
||||
//
|
||||
// Output is JSON on stdout: [ [ { bytePos, line, char }, ... ], ... ]
|
||||
const jsReferenceScript = `
|
||||
const inChunks = [];
|
||||
process.stdin.on('data', c => inChunks.push(c));
|
||||
process.stdin.on('end', () => {
|
||||
const buf = Buffer.concat(inChunks);
|
||||
let off = 0;
|
||||
const readU32 = () => { const v = buf.readUInt32LE(off); off += 4; return v; };
|
||||
const n = readU32();
|
||||
const buffers = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const len = readU32();
|
||||
buffers.push(buf.subarray(off, off + len));
|
||||
off += len;
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder('utf-8', { fatal: true });
|
||||
const out = buffers.map(bytes => {
|
||||
// Decode the raw UTF-8 bytes to a JS string (this is what sys.ts does with file contents).
|
||||
const text = decoder.decode(bytes);
|
||||
|
||||
// LSP line starts in the *decoded* JS string: \\n, \\r, \\r\\n only.
|
||||
const lineStartsJs = [0];
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text.charCodeAt(i);
|
||||
if (c === 13) {
|
||||
if (i + 1 < text.length && text.charCodeAt(i + 1) === 10) i++;
|
||||
lineStartsJs.push(i + 1);
|
||||
} else if (c === 10) {
|
||||
lineStartsJs.push(i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the original UTF-8 byte buffer to find codepoint boundaries. Inputs are
|
||||
// valid UTF-8, so we advance bytePos by the sequence length of each lead byte
|
||||
// and jsIdx by the corresponding UTF-16 code unit count (1 for BMP, 2 for
|
||||
// surrogate pair) of the codepoint at jsIdx in the decoded string.
|
||||
const boundaries = [{ bytePos: 0, jsIdx: 0 }];
|
||||
let bytePos = 0, jsIdx = 0;
|
||||
while (bytePos < bytes.length) {
|
||||
const seq = utf8SeqLen(bytes[bytePos]);
|
||||
const cp = text.codePointAt(jsIdx);
|
||||
bytePos += seq;
|
||||
jsIdx += cp > 0xFFFF ? 2 : 1;
|
||||
boundaries.push({ bytePos, jsIdx });
|
||||
}
|
||||
|
||||
return boundaries.map(({ bytePos, jsIdx }) => {
|
||||
let lo = 0, hi = lineStartsJs.length - 1;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi + 1) >> 1;
|
||||
if (lineStartsJs[mid] <= jsIdx) lo = mid;
|
||||
else hi = mid - 1;
|
||||
}
|
||||
return { bytePos, line: lo, char: jsIdx - lineStartsJs[lo] };
|
||||
});
|
||||
});
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
});
|
||||
|
||||
function utf8SeqLen(b) {
|
||||
if (b < 0x80) return 1;
|
||||
if ((b & 0xE0) === 0xC0) return 2;
|
||||
if ((b & 0xF0) === 0xE0) return 3;
|
||||
if ((b & 0xF8) === 0xF0) return 4;
|
||||
throw new Error('invalid UTF-8 lead byte 0x' + b.toString(16));
|
||||
}
|
||||
`
|
||||
|
||||
type jsTuple struct {
|
||||
BytePos int `json:"bytePos"`
|
||||
Line int `json:"line"`
|
||||
Char int `json:"char"`
|
||||
}
|
||||
|
||||
func runJSReference(t *testing.T, texts []string) [][]jsTuple {
|
||||
t.Helper()
|
||||
if _, err := exec.LookPath("node"); err != nil {
|
||||
t.Skipf("node not available: %v", err)
|
||||
}
|
||||
|
||||
// Build a length-prefixed binary stream of the raw UTF-8 bytes:
|
||||
// [uint32 LE count] then for each: [uint32 LE length][bytes].
|
||||
var in bytes.Buffer
|
||||
var u32 [4]byte
|
||||
binary.LittleEndian.PutUint32(u32[:], uint32(len(texts)))
|
||||
in.Write(u32[:])
|
||||
for _, s := range texts {
|
||||
binary.LittleEndian.PutUint32(u32[:], uint32(len(s)))
|
||||
in.Write(u32[:])
|
||||
in.WriteString(s)
|
||||
}
|
||||
|
||||
cmd := exec.Command("node", "-e", jsReferenceScript)
|
||||
cmd.Stdin = &in
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("node failed: %v\nstderr: %s", err, stderr.String())
|
||||
}
|
||||
|
||||
var out [][]jsTuple
|
||||
assert.NilError(t, json.Unmarshal(stdout.Bytes(), &out))
|
||||
return out
|
||||
}
|
||||
|
||||
// TestConvertersAgainstJSReference cross-checks the Go UTF-16 conversions against
|
||||
// authoritative results computed by Node.js using real UTF-16 string semantics.
|
||||
func TestConvertersAgainstJSReference(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
text string
|
||||
}{
|
||||
{"empty", ""},
|
||||
{"ascii", "hello\nworld"},
|
||||
{"ascii_crlf", "hello\r\nworld\r\n!"},
|
||||
{"ascii_cr_only", "a\rb\rc"},
|
||||
{"trailing_newline", "abc\n"},
|
||||
{"bmp_em_dash", "ab\u2014cd\nef"},
|
||||
{"bmp_multi", "α\nβ\nγδε\nzz"},
|
||||
{"supplementary_emoji", "x\U0001F600y\nz"}, // 😀 is 4 UTF-8 bytes, 2 UTF-16 units
|
||||
{"supplementary_at_lineend", "ab\U0001F600\ncd\U0001F60A"},
|
||||
{"supplementary_only", "\U0001F600\U0001F601\U0001F602"},
|
||||
{"mixed", "α — \U0001F600\r\nβ\nγ\r"},
|
||||
{"long_mixed_ws", " \tαβ\n\t\U0001F600 end\n"},
|
||||
{"zwj_emoji", "\U0001F468\u200D\U0001F4BB\nnext"},
|
||||
{"only_newlines", "\n\n\r\n\r"},
|
||||
}
|
||||
|
||||
texts := make([]string, len(cases))
|
||||
for i, c := range cases {
|
||||
texts[i] = c.text
|
||||
}
|
||||
refs := runJSReference(t, texts)
|
||||
assert.Equal(t, len(refs), len(cases))
|
||||
|
||||
for i, c := range cases {
|
||||
ref := refs[i]
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conv, script := newTestConverters(c.text)
|
||||
for _, tup := range ref {
|
||||
bytePos := core.TextPos(tup.BytePos)
|
||||
expectedLC := lsproto.Position{Line: uint32(tup.Line), Character: uint32(tup.Char)}
|
||||
|
||||
gotLC := conv.PositionToLineAndCharacter(script, bytePos)
|
||||
assert.Equal(t, gotLC, expectedLC,
|
||||
fmt.Sprintf("PositionToLineAndCharacter(%d) mismatch in %q", bytePos, c.text))
|
||||
|
||||
gotPos := conv.LineAndCharacterToPosition(script, expectedLC)
|
||||
assert.Equal(t, gotPos, bytePos,
|
||||
fmt.Sprintf("LineAndCharacterToPosition(%d,%d) mismatch in %q", tup.Line, tup.Char, c.text))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
71
tools/tsgo/internal/ls/lsconv/linemap.go
Normal file
71
tools/tsgo/internal/ls/lsconv/linemap.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package lsconv
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
)
|
||||
|
||||
type LSPLineStarts []core.TextPos
|
||||
|
||||
type LSPLineMap struct {
|
||||
LineStarts LSPLineStarts
|
||||
AsciiOnly bool // TODO(jakebailey): collect ascii-only info per line
|
||||
}
|
||||
|
||||
func ComputeLSPLineStarts(text string) *LSPLineMap {
|
||||
// This is like core.ComputeLineStarts, but only considers "\n", "\r", and "\r\n" as line breaks,
|
||||
// and reports when the text is ASCII-only.
|
||||
lineStarts := make([]core.TextPos, 0, strings.Count(text, "\n")+1)
|
||||
asciiOnly := true
|
||||
|
||||
textLen := core.TextPos(len(text))
|
||||
var pos core.TextPos
|
||||
var lineStart core.TextPos
|
||||
for pos < textLen {
|
||||
b := text[pos]
|
||||
if b < utf8.RuneSelf {
|
||||
pos++
|
||||
switch b {
|
||||
case '\r':
|
||||
if pos < textLen && text[pos] == '\n' {
|
||||
pos++
|
||||
}
|
||||
fallthrough
|
||||
case '\n':
|
||||
lineStarts = append(lineStarts, lineStart)
|
||||
lineStart = pos
|
||||
}
|
||||
} else {
|
||||
_, size := utf8.DecodeRuneInString(text[pos:])
|
||||
pos += core.TextPos(size)
|
||||
asciiOnly = false
|
||||
}
|
||||
}
|
||||
lineStarts = append(lineStarts, lineStart)
|
||||
|
||||
return &LSPLineMap{
|
||||
LineStarts: lineStarts,
|
||||
AsciiOnly: asciiOnly,
|
||||
}
|
||||
}
|
||||
|
||||
func (lm *LSPLineMap) ComputeIndexOfLineStart(targetPos core.TextPos) int {
|
||||
// port of computeLineOfPosition(lineStarts: readonly number[], position: number, lowerBound?: number): number {
|
||||
lineNumber, ok := slices.BinarySearchFunc(lm.LineStarts, targetPos, func(p, t core.TextPos) int {
|
||||
return cmp.Compare(int(p), int(t))
|
||||
})
|
||||
if !ok && lineNumber > 0 {
|
||||
// If the actual position was not found, the binary search returns where the target line start would be inserted
|
||||
// if the target was in the slice.
|
||||
// e.g. if the line starts at [5, 10, 23, 80] and the position requested was 20
|
||||
// then the search will return (3, false).
|
||||
//
|
||||
// We want the index of the previous line start, so we subtract 1.
|
||||
lineNumber = lineNumber - 1
|
||||
}
|
||||
return lineNumber
|
||||
}
|
||||
104
tools/tsgo/internal/ls/lsutil/asi.go
Normal file
104
tools/tsgo/internal/ls/lsutil/asi.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
func PositionIsASICandidate(pos int, context *ast.Node, file *ast.SourceFile) bool {
|
||||
contextAncestor := ast.FindAncestorOrQuit(context, func(ancestor *ast.Node) ast.FindAncestorResult {
|
||||
if ancestor.End() != pos {
|
||||
return ast.FindAncestorQuit
|
||||
}
|
||||
|
||||
return ast.ToFindAncestorResult(SyntaxMayBeASICandidate(ancestor.Kind))
|
||||
})
|
||||
|
||||
return contextAncestor != nil && NodeIsASICandidate(contextAncestor, file)
|
||||
}
|
||||
|
||||
func SyntaxMayBeASICandidate(kind ast.Kind) bool {
|
||||
return SyntaxRequiresTrailingCommaOrSemicolonOrASI(kind) ||
|
||||
SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind) ||
|
||||
SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind) ||
|
||||
SyntaxRequiresTrailingSemicolonOrASI(kind)
|
||||
}
|
||||
|
||||
func SyntaxRequiresTrailingCommaOrSemicolonOrASI(kind ast.Kind) bool {
|
||||
return kind == ast.KindCallSignature ||
|
||||
kind == ast.KindConstructSignature ||
|
||||
kind == ast.KindIndexSignature ||
|
||||
kind == ast.KindPropertySignature ||
|
||||
kind == ast.KindMethodSignature
|
||||
}
|
||||
|
||||
func SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind ast.Kind) bool {
|
||||
return kind == ast.KindFunctionDeclaration ||
|
||||
kind == ast.KindConstructor ||
|
||||
kind == ast.KindMethodDeclaration ||
|
||||
kind == ast.KindGetAccessor ||
|
||||
kind == ast.KindSetAccessor
|
||||
}
|
||||
|
||||
func SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind ast.Kind) bool {
|
||||
return kind == ast.KindModuleDeclaration
|
||||
}
|
||||
|
||||
func SyntaxRequiresTrailingSemicolonOrASI(kind ast.Kind) bool {
|
||||
return kind == ast.KindVariableStatement ||
|
||||
kind == ast.KindExpressionStatement ||
|
||||
kind == ast.KindDoStatement ||
|
||||
kind == ast.KindContinueStatement ||
|
||||
kind == ast.KindBreakStatement ||
|
||||
kind == ast.KindReturnStatement ||
|
||||
kind == ast.KindThrowStatement ||
|
||||
kind == ast.KindDebuggerStatement ||
|
||||
kind == ast.KindPropertyDeclaration ||
|
||||
kind == ast.KindTypeAliasDeclaration ||
|
||||
kind == ast.KindImportDeclaration ||
|
||||
kind == ast.KindImportEqualsDeclaration ||
|
||||
kind == ast.KindExportDeclaration ||
|
||||
kind == ast.KindNamespaceExportDeclaration ||
|
||||
kind == ast.KindExportAssignment
|
||||
}
|
||||
|
||||
func NodeIsASICandidate(node *ast.Node, file *ast.SourceFile) bool {
|
||||
lastToken := GetLastToken(node, file)
|
||||
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
|
||||
return false
|
||||
}
|
||||
|
||||
if SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) {
|
||||
if lastToken != nil && lastToken.Kind == ast.KindCommaToken {
|
||||
return false
|
||||
}
|
||||
} else if SyntaxRequiresTrailingModuleBlockOrSemicolonOrASI(node.Kind) {
|
||||
lastChild := GetLastChild(node, file)
|
||||
if lastChild != nil && ast.IsModuleBlock(lastChild) {
|
||||
return false
|
||||
}
|
||||
} else if SyntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(node.Kind) {
|
||||
lastChild := GetLastChild(node, file)
|
||||
if lastChild != nil && ast.IsFunctionBlock(lastChild) {
|
||||
return false
|
||||
}
|
||||
} else if !SyntaxRequiresTrailingSemicolonOrASI(node.Kind) {
|
||||
return false
|
||||
}
|
||||
|
||||
// See comment in parser's `parseDoStatement`
|
||||
if node.Kind == ast.KindDoStatement {
|
||||
return true
|
||||
}
|
||||
|
||||
topNode := ast.FindAncestor(node, func(ancestor *ast.Node) bool { return ancestor.Parent == nil })
|
||||
nextToken := astnav.FindNextToken(node, topNode, file)
|
||||
if nextToken == nil || nextToken.Kind == ast.KindCloseBraceToken {
|
||||
return true
|
||||
}
|
||||
|
||||
startLine := scanner.GetECMALineOfPosition(file, node.End())
|
||||
endLine := scanner.GetECMALineOfPosition(file, astnav.GetStartOfNode(nextToken, file, false /*includeJSDoc*/))
|
||||
return startLine != endLine
|
||||
}
|
||||
130
tools/tsgo/internal/ls/lsutil/children.go
Normal file
130
tools/tsgo/internal/ls/lsutil/children.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
// Replaces last(node.getChildren(sourceFile))
|
||||
func GetLastChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
|
||||
lastChildNode := GetLastVisitedChild(node, sourceFile)
|
||||
if ast.IsJSDocSingleCommentNode(node) && lastChildNode == nil {
|
||||
return nil
|
||||
}
|
||||
var tokenStartPos int
|
||||
if lastChildNode != nil {
|
||||
tokenStartPos = lastChildNode.End()
|
||||
} else {
|
||||
tokenStartPos = node.Pos()
|
||||
}
|
||||
var lastToken *ast.Node
|
||||
scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos)
|
||||
for startPos := tokenStartPos; startPos < node.End(); {
|
||||
tokenKind := scanner.Token()
|
||||
tokenFullStart := scanner.TokenFullStart()
|
||||
tokenEnd := scanner.TokenEnd()
|
||||
lastToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node, scanner.TokenFlags())
|
||||
startPos = tokenEnd
|
||||
scanner.Scan()
|
||||
}
|
||||
return core.IfElse(lastToken != nil, lastToken, lastChildNode)
|
||||
}
|
||||
|
||||
func GetLastToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ast.IsTokenKind(node.Kind) || ast.IsIdentifier(node) {
|
||||
return nil
|
||||
}
|
||||
|
||||
AssertHasRealPosition(node)
|
||||
|
||||
lastChild := GetLastChild(node, sourceFile)
|
||||
if lastChild == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if lastChild.Kind < ast.KindFirstNode {
|
||||
return lastChild
|
||||
} else {
|
||||
return GetLastToken(lastChild, sourceFile)
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the last visited child of the given node.
|
||||
// NOTE: This doesn't include unvisited tokens; for this, use `getLastChild` or `getLastToken`.
|
||||
func GetLastVisitedChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
|
||||
var lastChild *ast.Node
|
||||
|
||||
visitNode := func(n *ast.Node, _ *ast.NodeVisitor) *ast.Node {
|
||||
if n != nil && n.Flags&ast.NodeFlagsReparsed == 0 {
|
||||
lastChild = n
|
||||
}
|
||||
return n
|
||||
}
|
||||
visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList {
|
||||
if nodeList != nil && len(nodeList.Nodes) > 0 {
|
||||
for i := len(nodeList.Nodes) - 1; i >= 0; i-- {
|
||||
if nodeList.Nodes[i].Flags&ast.NodeFlagsReparsed == 0 {
|
||||
lastChild = nodeList.Nodes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodeList
|
||||
}
|
||||
|
||||
astnav.VisitEachChildAndJSDoc(node, sourceFile, visitNode, visitNodeList)
|
||||
return lastChild
|
||||
}
|
||||
|
||||
func GetFirstToken(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node {
|
||||
if ast.IsIdentifier(node) || ast.IsTokenKind(node.Kind) {
|
||||
return nil
|
||||
}
|
||||
AssertHasRealPosition(node)
|
||||
var firstChild *ast.Node
|
||||
node.ForEachChild(func(n *ast.Node) bool {
|
||||
if n == nil || node.Flags&ast.NodeFlagsReparsed != 0 {
|
||||
return false
|
||||
}
|
||||
firstChild = n
|
||||
return true
|
||||
})
|
||||
|
||||
var tokenEndPosition int
|
||||
if firstChild != nil {
|
||||
tokenEndPosition = firstChild.Pos()
|
||||
} else {
|
||||
tokenEndPosition = node.End()
|
||||
}
|
||||
scanner := scanner.GetScannerForSourceFile(sourceFile, node.Pos())
|
||||
var firstToken *ast.Node
|
||||
if node.Pos() < tokenEndPosition {
|
||||
tokenKind := scanner.Token()
|
||||
tokenFullStart := scanner.TokenFullStart()
|
||||
tokenEnd := scanner.TokenEnd()
|
||||
firstToken = sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, node, scanner.TokenFlags())
|
||||
}
|
||||
|
||||
if firstToken != nil {
|
||||
return firstToken
|
||||
}
|
||||
if firstChild == nil {
|
||||
return nil
|
||||
}
|
||||
if firstChild.Kind < ast.KindFirstNode {
|
||||
return firstChild
|
||||
}
|
||||
return GetFirstToken(firstChild, sourceFile)
|
||||
}
|
||||
|
||||
func AssertHasRealPosition(node *ast.Node) {
|
||||
if ast.PositionIsSynthesized(node.Pos()) || ast.PositionIsSynthesized(node.End()) {
|
||||
panic("Node must have a real position for this operation.")
|
||||
}
|
||||
}
|
||||
196
tools/tsgo/internal/ls/lsutil/completednode.go
Normal file
196
tools/tsgo/internal/ls/lsutil/completednode.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
// PositionBelongsToNode returns true if the position belongs to the node.
|
||||
// Assumes `candidate.Pos() <= position` holds.
|
||||
func PositionBelongsToNode(candidate *ast.Node, position int, file *ast.SourceFile) bool {
|
||||
if candidate.Pos() > position {
|
||||
panic("Expected candidate.pos <= position")
|
||||
}
|
||||
return position < candidate.End() || !IsCompletedNode(candidate, file)
|
||||
}
|
||||
|
||||
func IsCompletedNode(n *ast.Node, sourceFile *ast.SourceFile) bool {
|
||||
if n == nil || ast.NodeIsMissing(n) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch n.Kind {
|
||||
case ast.KindClassDeclaration,
|
||||
ast.KindInterfaceDeclaration,
|
||||
ast.KindEnumDeclaration,
|
||||
ast.KindObjectLiteralExpression,
|
||||
ast.KindObjectBindingPattern,
|
||||
ast.KindTypeLiteral,
|
||||
ast.KindBlock,
|
||||
ast.KindModuleBlock,
|
||||
ast.KindCaseBlock,
|
||||
ast.KindNamedImports,
|
||||
ast.KindNamedExports:
|
||||
return nodeEndsWith(n, ast.KindCloseBraceToken, sourceFile)
|
||||
|
||||
case ast.KindCatchClause:
|
||||
return IsCompletedNode(n.AsCatchClause().Block, sourceFile)
|
||||
|
||||
case ast.KindNewExpression:
|
||||
if n.ArgumentList() == nil {
|
||||
return true
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case ast.KindCallExpression,
|
||||
ast.KindParenthesizedExpression,
|
||||
ast.KindParenthesizedType:
|
||||
return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile)
|
||||
|
||||
case ast.KindFunctionType,
|
||||
ast.KindConstructorType:
|
||||
return IsCompletedNode(n.Type(), sourceFile)
|
||||
|
||||
case ast.KindConstructor,
|
||||
ast.KindGetAccessor,
|
||||
ast.KindSetAccessor,
|
||||
ast.KindFunctionDeclaration,
|
||||
ast.KindFunctionExpression,
|
||||
ast.KindMethodDeclaration,
|
||||
ast.KindMethodSignature,
|
||||
ast.KindConstructSignature,
|
||||
ast.KindCallSignature,
|
||||
ast.KindArrowFunction:
|
||||
if n.Body() != nil {
|
||||
return IsCompletedNode(n.Body(), sourceFile)
|
||||
}
|
||||
if n.Type() != nil {
|
||||
return IsCompletedNode(n.Type(), sourceFile)
|
||||
}
|
||||
// Even though type parameters can be unclosed, we can get away with
|
||||
// having at least a closing paren.
|
||||
return hasChildOfKind(n, ast.KindCloseParenToken, sourceFile)
|
||||
|
||||
case ast.KindModuleDeclaration:
|
||||
return n.Body() != nil && IsCompletedNode(n.Body(), sourceFile)
|
||||
|
||||
case ast.KindIfStatement:
|
||||
if n.AsIfStatement().ElseStatement != nil {
|
||||
return IsCompletedNode(n.AsIfStatement().ElseStatement, sourceFile)
|
||||
}
|
||||
return IsCompletedNode(n.AsIfStatement().ThenStatement, sourceFile)
|
||||
|
||||
case ast.KindExpressionStatement:
|
||||
return IsCompletedNode(n.Expression(), sourceFile) ||
|
||||
hasChildOfKind(n, ast.KindSemicolonToken, sourceFile)
|
||||
|
||||
case ast.KindArrayLiteralExpression,
|
||||
ast.KindArrayBindingPattern,
|
||||
ast.KindElementAccessExpression,
|
||||
ast.KindComputedPropertyName,
|
||||
ast.KindTupleType:
|
||||
return nodeEndsWith(n, ast.KindCloseBracketToken, sourceFile)
|
||||
|
||||
case ast.KindIndexSignature:
|
||||
if n.AsIndexSignatureDeclaration().Type != nil {
|
||||
return IsCompletedNode(n.AsIndexSignatureDeclaration().Type, sourceFile)
|
||||
}
|
||||
return hasChildOfKind(n, ast.KindCloseBracketToken, sourceFile)
|
||||
|
||||
case ast.KindCaseClause,
|
||||
ast.KindDefaultClause:
|
||||
// there is no such thing as terminator token for CaseClause/DefaultClause so for simplicity always consider them non-completed
|
||||
return false
|
||||
|
||||
case ast.KindForStatement,
|
||||
ast.KindForInStatement,
|
||||
ast.KindForOfStatement,
|
||||
ast.KindWhileStatement:
|
||||
return IsCompletedNode(n.Statement(), sourceFile)
|
||||
case ast.KindDoStatement:
|
||||
// rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')';
|
||||
if hasChildOfKind(n, ast.KindWhileKeyword, sourceFile) {
|
||||
return nodeEndsWith(n, ast.KindCloseParenToken, sourceFile)
|
||||
}
|
||||
return IsCompletedNode(n.Statement(), sourceFile)
|
||||
|
||||
case ast.KindTypeQuery:
|
||||
return IsCompletedNode(n.AsTypeQueryNode().ExprName, sourceFile)
|
||||
|
||||
case ast.KindTypeOfExpression,
|
||||
ast.KindDeleteExpression,
|
||||
ast.KindVoidExpression,
|
||||
ast.KindYieldExpression,
|
||||
ast.KindSpreadElement:
|
||||
return IsCompletedNode(n.Expression(), sourceFile)
|
||||
|
||||
case ast.KindTaggedTemplateExpression:
|
||||
return IsCompletedNode(n.AsTaggedTemplateExpression().Template, sourceFile)
|
||||
|
||||
case ast.KindTemplateExpression:
|
||||
if n.AsTemplateExpression().TemplateSpans == nil {
|
||||
return false
|
||||
}
|
||||
lastSpan := core.LastOrNil(n.AsTemplateExpression().TemplateSpans.Nodes)
|
||||
return IsCompletedNode(lastSpan, sourceFile)
|
||||
|
||||
case ast.KindTemplateSpan:
|
||||
return ast.NodeIsPresent(n.AsTemplateSpan().Literal)
|
||||
|
||||
case ast.KindExportDeclaration,
|
||||
ast.KindImportDeclaration:
|
||||
return ast.NodeIsPresent(n.ModuleSpecifier())
|
||||
|
||||
case ast.KindPrefixUnaryExpression:
|
||||
return IsCompletedNode(n.AsPrefixUnaryExpression().Operand, sourceFile)
|
||||
|
||||
case ast.KindBinaryExpression:
|
||||
return IsCompletedNode(n.AsBinaryExpression().Right, sourceFile)
|
||||
|
||||
case ast.KindConditionalExpression:
|
||||
return IsCompletedNode(n.AsConditionalExpression().WhenFalse, sourceFile)
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if node ends with 'expectedLastToken'.
|
||||
// If child at position 'length - 1' is 'SemicolonToken' it is skipped and 'expectedLastToken' is compared with child at position 'length - 2'.
|
||||
func nodeEndsWith(n *ast.Node, expectedLastToken ast.Kind, sourceFile *ast.SourceFile) bool {
|
||||
lastChildNode := GetLastVisitedChild(n, sourceFile)
|
||||
var lastNodeAndTokens []*ast.Node
|
||||
var tokenStartPos int
|
||||
if lastChildNode != nil {
|
||||
lastNodeAndTokens = []*ast.Node{lastChildNode}
|
||||
tokenStartPos = lastChildNode.End()
|
||||
} else {
|
||||
tokenStartPos = n.Pos()
|
||||
}
|
||||
scanner := scanner.GetScannerForSourceFile(sourceFile, tokenStartPos)
|
||||
for startPos := tokenStartPos; startPos < n.End(); {
|
||||
tokenKind := scanner.Token()
|
||||
tokenFullStart := scanner.TokenFullStart()
|
||||
tokenEnd := scanner.TokenEnd()
|
||||
token := sourceFile.GetOrCreateToken(tokenKind, tokenFullStart, tokenEnd, n, scanner.TokenFlags())
|
||||
lastNodeAndTokens = append(lastNodeAndTokens, token)
|
||||
startPos = tokenEnd
|
||||
scanner.Scan()
|
||||
}
|
||||
if len(lastNodeAndTokens) == 0 {
|
||||
return false
|
||||
}
|
||||
lastChild := lastNodeAndTokens[len(lastNodeAndTokens)-1]
|
||||
if lastChild.Kind == expectedLastToken {
|
||||
return true
|
||||
} else if lastChild.Kind == ast.KindSemicolonToken && len(lastNodeAndTokens) > 1 {
|
||||
return lastNodeAndTokens[len(lastNodeAndTokens)-2].Kind == expectedLastToken
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasChildOfKind(containingNode *ast.Node, kind ast.Kind, sourceFile *ast.SourceFile) bool {
|
||||
return astnav.FindChildOfKind(containingNode, kind, sourceFile) != nil
|
||||
}
|
||||
141
tools/tsgo/internal/ls/lsutil/formatcodeoptions.go
Normal file
141
tools/tsgo/internal/ls/lsutil/formatcodeoptions.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
)
|
||||
|
||||
type IndentStyle int
|
||||
|
||||
const (
|
||||
IndentStyleNone IndentStyle = iota
|
||||
IndentStyleBlock
|
||||
IndentStyleSmart
|
||||
)
|
||||
|
||||
func parseIndentStyle(v any) IndentStyle {
|
||||
switch s := v.(type) {
|
||||
case string:
|
||||
switch strings.ToLower(s) {
|
||||
case "none":
|
||||
return IndentStyleNone
|
||||
case "block":
|
||||
return IndentStyleBlock
|
||||
case "smart":
|
||||
return IndentStyleSmart
|
||||
}
|
||||
case float64:
|
||||
return IndentStyle(int(s))
|
||||
case int:
|
||||
return IndentStyle(s)
|
||||
}
|
||||
return IndentStyleSmart
|
||||
}
|
||||
|
||||
type SemicolonPreference string
|
||||
|
||||
const (
|
||||
SemicolonPreferenceIgnore SemicolonPreference = "ignore"
|
||||
SemicolonPreferenceInsert SemicolonPreference = "insert"
|
||||
SemicolonPreferenceRemove SemicolonPreference = "remove"
|
||||
)
|
||||
|
||||
func parseSemicolonPreference(v any) SemicolonPreference {
|
||||
if s, ok := v.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "ignore":
|
||||
return SemicolonPreferenceIgnore
|
||||
case "insert":
|
||||
return SemicolonPreferenceInsert
|
||||
case "remove":
|
||||
return SemicolonPreferenceRemove
|
||||
}
|
||||
}
|
||||
return SemicolonPreferenceIgnore
|
||||
}
|
||||
|
||||
type EditorSettings struct {
|
||||
BaseIndentSize int `raw:"baseIndentSize" config:"format.baseIndentSize"`
|
||||
IndentSize int `raw:"indentSize" config:"format.indentSize"`
|
||||
TabSize int `raw:"tabSize" config:"format.tabSize"`
|
||||
NewLineCharacter string `raw:"newLineCharacter" config:"format.newLineCharacter"`
|
||||
ConvertTabsToSpaces core.Tristate `raw:"convertTabsToSpaces" config:"format.convertTabsToSpaces"`
|
||||
IndentStyle IndentStyle `raw:"indentStyle" config:"format.indentStyle"`
|
||||
TrimTrailingWhitespace core.Tristate `raw:"trimTrailingWhitespace" config:"format.trimTrailingWhitespace"`
|
||||
}
|
||||
|
||||
type FormatCodeSettings struct {
|
||||
EditorSettings
|
||||
InsertSpaceAfterCommaDelimiter core.Tristate `raw:"insertSpaceAfterCommaDelimiter" config:"format.insertSpaceAfterCommaDelimiter"`
|
||||
InsertSpaceAfterSemicolonInForStatements core.Tristate `raw:"insertSpaceAfterSemicolonInForStatements" config:"format.insertSpaceAfterSemicolonInForStatements"`
|
||||
InsertSpaceBeforeAndAfterBinaryOperators core.Tristate `raw:"insertSpaceBeforeAndAfterBinaryOperators" config:"format.insertSpaceBeforeAndAfterBinaryOperators"`
|
||||
InsertSpaceAfterConstructor core.Tristate `raw:"insertSpaceAfterConstructor" config:"format.insertSpaceAfterConstructor"`
|
||||
InsertSpaceAfterKeywordsInControlFlowStatements core.Tristate `raw:"insertSpaceAfterKeywordsInControlFlowStatements" config:"format.insertSpaceAfterKeywordsInControlFlowStatements"`
|
||||
InsertSpaceAfterFunctionKeywordForAnonymousFunctions core.Tristate `raw:"insertSpaceAfterFunctionKeywordForAnonymousFunctions" config:"format.insertSpaceAfterFunctionKeywordForAnonymousFunctions"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingEmptyBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"`
|
||||
InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces core.Tristate `raw:"insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces" config:"format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"`
|
||||
InsertSpaceAfterTypeAssertion core.Tristate `raw:"insertSpaceAfterTypeAssertion" config:"format.insertSpaceAfterTypeAssertion"`
|
||||
InsertSpaceBeforeFunctionParenthesis core.Tristate `raw:"insertSpaceBeforeFunctionParenthesis" config:"format.insertSpaceBeforeFunctionParenthesis"`
|
||||
PlaceOpenBraceOnNewLineForFunctions core.Tristate `raw:"placeOpenBraceOnNewLineForFunctions" config:"format.placeOpenBraceOnNewLineForFunctions"`
|
||||
PlaceOpenBraceOnNewLineForControlBlocks core.Tristate `raw:"placeOpenBraceOnNewLineForControlBlocks" config:"format.placeOpenBraceOnNewLineForControlBlocks"`
|
||||
InsertSpaceBeforeTypeAnnotation core.Tristate `raw:"insertSpaceBeforeTypeAnnotation" config:"format.insertSpaceBeforeTypeAnnotation"`
|
||||
IndentMultiLineObjectLiteralBeginningOnBlankLine core.Tristate `raw:"indentMultiLineObjectLiteralBeginningOnBlankLine" config:"format.indentMultiLineObjectLiteralBeginningOnBlankLine"`
|
||||
Semicolons SemicolonPreference `raw:"semicolons" config:"format.semicolons"`
|
||||
IndentSwitchCase core.Tristate `raw:"indentSwitchCase" config:"format.indentSwitchCase"`
|
||||
}
|
||||
|
||||
func FromLSFormatOptions(f FormatCodeSettings, opt *lsproto.FormattingOptions) FormatCodeSettings {
|
||||
updatedSettings := f
|
||||
updatedSettings.TabSize = int(opt.TabSize)
|
||||
updatedSettings.IndentSize = int(opt.TabSize)
|
||||
updatedSettings.ConvertTabsToSpaces = core.BoolToTristate(opt.InsertSpaces)
|
||||
if opt.TrimTrailingWhitespace != nil {
|
||||
updatedSettings.TrimTrailingWhitespace = core.BoolToTristate(*opt.TrimTrailingWhitespace)
|
||||
}
|
||||
return updatedSettings
|
||||
}
|
||||
|
||||
func (settings FormatCodeSettings) ToLSFormatOptions() *lsproto.FormattingOptions {
|
||||
trimTrailingWhitespace := settings.TrimTrailingWhitespace.IsTrue()
|
||||
return &lsproto.FormattingOptions{
|
||||
TabSize: uint32(settings.TabSize),
|
||||
InsertSpaces: settings.ConvertTabsToSpaces.IsTrue(),
|
||||
TrimTrailingWhitespace: &trimTrailingWhitespace,
|
||||
}
|
||||
}
|
||||
|
||||
func GetDefaultFormatCodeSettings() FormatCodeSettings {
|
||||
return FormatCodeSettings{
|
||||
EditorSettings: EditorSettings{
|
||||
IndentSize: printer.GetDefaultIndentSize(),
|
||||
TabSize: printer.GetDefaultIndentSize(),
|
||||
NewLineCharacter: "\n",
|
||||
ConvertTabsToSpaces: core.TSTrue,
|
||||
IndentStyle: IndentStyleSmart,
|
||||
TrimTrailingWhitespace: core.TSTrue,
|
||||
},
|
||||
InsertSpaceAfterConstructor: core.TSFalse,
|
||||
InsertSpaceAfterCommaDelimiter: core.TSTrue,
|
||||
InsertSpaceAfterSemicolonInForStatements: core.TSTrue,
|
||||
InsertSpaceBeforeAndAfterBinaryOperators: core.TSTrue,
|
||||
InsertSpaceAfterKeywordsInControlFlowStatements: core.TSTrue,
|
||||
InsertSpaceAfterFunctionKeywordForAnonymousFunctions: core.TSFalse,
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: core.TSFalse,
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: core.TSFalse,
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: core.TSTrue,
|
||||
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: core.TSFalse,
|
||||
InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: core.TSFalse,
|
||||
InsertSpaceBeforeFunctionParenthesis: core.TSFalse,
|
||||
PlaceOpenBraceOnNewLineForFunctions: core.TSFalse,
|
||||
PlaceOpenBraceOnNewLineForControlBlocks: core.TSFalse,
|
||||
Semicolons: SemicolonPreferenceIgnore,
|
||||
IndentSwitchCase: core.TSTrue,
|
||||
}
|
||||
}
|
||||
695
tools/tsgo/internal/ls/lsutil/organizeimports.go
Normal file
695
tools/tsgo/internal/ls/lsutil/organizeimports.go
Normal file
@@ -0,0 +1,695 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"math"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// FilterImportDeclarations filters out non-import declarations from a list of statements.
|
||||
func FilterImportDeclarations(statements []*ast.Statement) []*ast.Statement {
|
||||
return core.Filter(statements, func(stmt *ast.Statement) bool {
|
||||
return stmt.Kind == ast.KindImportDeclaration
|
||||
})
|
||||
}
|
||||
|
||||
// GetDetectionLists returns the lists of comparers and type orders to test for organize imports detection.
|
||||
func GetDetectionLists(preferences UserPreferences) (comparersToTest []func(a, b string) int, typeOrdersToTest []OrganizeImportsTypeOrder) {
|
||||
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
|
||||
comparersToTest = []func(a, b string) int{getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort)}
|
||||
} else if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
|
||||
comparersToTest = []func(a, b string) int{getOrganizeImportsStringComparer(preferences, preferences.OrganizeImportsIgnoreCase.IsTrue())}
|
||||
} else {
|
||||
comparersToTest = []func(a, b string) int{
|
||||
getOrganizeImportsStringComparer(preferences, true),
|
||||
getOrganizeImportsStringComparer(preferences, false),
|
||||
}
|
||||
}
|
||||
|
||||
if preferences.OrganizeImportsTypeOrder != OrganizeImportsTypeOrderAuto {
|
||||
typeOrdersToTest = []OrganizeImportsTypeOrder{preferences.OrganizeImportsTypeOrder}
|
||||
} else {
|
||||
typeOrdersToTest = []OrganizeImportsTypeOrder{
|
||||
OrganizeImportsTypeOrderLast,
|
||||
OrganizeImportsTypeOrderInline,
|
||||
OrganizeImportsTypeOrderFirst,
|
||||
}
|
||||
}
|
||||
|
||||
return comparersToTest, typeOrdersToTest
|
||||
}
|
||||
|
||||
func ResolveOrganizeImportsSort(preferences UserPreferences) OrganizeImportsSort {
|
||||
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
|
||||
return preferences.OrganizeImportsSort
|
||||
}
|
||||
|
||||
if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode {
|
||||
switch preferences.OrganizeImportsIgnoreCase {
|
||||
case core.TSTrue:
|
||||
return OrganizeImportsSortNaturalIgnoreCase
|
||||
case core.TSFalse:
|
||||
return OrganizeImportsSortNatural
|
||||
default:
|
||||
return OrganizeImportsSortAuto
|
||||
}
|
||||
}
|
||||
|
||||
switch preferences.OrganizeImportsIgnoreCase {
|
||||
case core.TSTrue:
|
||||
return OrganizeImportsSortOrdinalIgnoreCase
|
||||
case core.TSFalse:
|
||||
return OrganizeImportsSortOrdinal
|
||||
default:
|
||||
return OrganizeImportsSortAuto
|
||||
}
|
||||
}
|
||||
|
||||
func getOrganizeImportsOrdinalStringComparer(ignoreCase bool) func(a, b string) int {
|
||||
if ignoreCase {
|
||||
return stringutil.CompareStringsCaseInsensitiveEslintCompatible
|
||||
}
|
||||
return stringutil.CompareStringsCaseSensitive
|
||||
}
|
||||
|
||||
func getOrganizeImportsNaturalStringComparer(caseSensitive bool) func(a, b string) int {
|
||||
return func(a, b string) int {
|
||||
return compareOrganizeImportsNaturalStrings(a, b, caseSensitive)
|
||||
}
|
||||
}
|
||||
|
||||
func getOrganizeImportsUnicodeStringComparer(ignoreCase bool, preferences UserPreferences) func(a, b string) int {
|
||||
caseFirst := preferences.OrganizeImportsCaseFirst
|
||||
numeric := preferences.OrganizeImportsNumericCollation.IsTrue()
|
||||
accents := !preferences.OrganizeImportsAccentCollation.IsFalse()
|
||||
|
||||
return func(a, b string) int {
|
||||
return compareOrganizeImportsUnicodeStrings(a, b, ignoreCase, caseFirst, numeric, accents)
|
||||
}
|
||||
}
|
||||
|
||||
func compareOrganizeImportsNaturalStrings(a string, b string, caseSensitive bool) int {
|
||||
if cmp := compareStringsNumeric(naturalCollationKey(a), naturalCollationKey(b)); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
|
||||
if caseSensitive {
|
||||
if cmp := compareOrganizeImportsCaseUpperFirst(a, b); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
func compareOrganizeImportsUnicodeStrings(a string, b string, ignoreCase bool, caseFirst OrganizeImportsCaseFirst, numeric bool, accents bool) int {
|
||||
if cmp := compareOrganizeImportsUnicodeKeys(naturalCollationKey(a), naturalCollationKey(b), numeric); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
|
||||
if accents {
|
||||
if cmp := compareOrganizeImportsUnicodeKeys(strings.ToLower(a), strings.ToLower(b), numeric); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
}
|
||||
|
||||
if !ignoreCase {
|
||||
if cmp := compareOrganizeImportsCase(a, b, caseFirst); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
func naturalCollationKey(s string) string {
|
||||
return strings.ToLower(removeDiacritics(s))
|
||||
}
|
||||
|
||||
func removeDiacritics(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if unicode.Is(unicode.Mn, r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, norm.NFD.String(s))
|
||||
}
|
||||
|
||||
func compareOrganizeImportsUnicodeKeys(a string, b string, numeric bool) int {
|
||||
if numeric {
|
||||
return compareStringsNumeric(a, b)
|
||||
}
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
func compareStringsNumeric(a string, b string) int {
|
||||
for len(a) > 0 && len(b) > 0 {
|
||||
if isASCIIDigit(a[0]) && isASCIIDigit(b[0]) {
|
||||
aRunEnd := asciiDigitRunEnd(a)
|
||||
bRunEnd := asciiDigitRunEnd(b)
|
||||
|
||||
if cmp := compareNumericText(a[:aRunEnd], b[:bRunEnd]); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
|
||||
a = a[aRunEnd:]
|
||||
b = b[bRunEnd:]
|
||||
continue
|
||||
}
|
||||
|
||||
aRune, aSize := utf8.DecodeRuneInString(a)
|
||||
bRune, bSize := utf8.DecodeRuneInString(b)
|
||||
if aRune != bRune {
|
||||
return cmp.Compare(aRune, bRune)
|
||||
}
|
||||
|
||||
a = a[aSize:]
|
||||
b = b[bSize:]
|
||||
}
|
||||
|
||||
return cmp.Compare(len(a), len(b))
|
||||
}
|
||||
|
||||
func isASCIIDigit(ch byte) bool {
|
||||
return ch >= '0' && ch <= '9'
|
||||
}
|
||||
|
||||
func asciiDigitRunEnd(s string) int {
|
||||
i := 0
|
||||
for i < len(s) && isASCIIDigit(s[i]) {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func compareNumericText(a string, b string) int {
|
||||
aDigits := strings.TrimLeft(a, "0")
|
||||
bDigits := strings.TrimLeft(b, "0")
|
||||
if aDigits == "" {
|
||||
aDigits = "0"
|
||||
}
|
||||
if bDigits == "" {
|
||||
bDigits = "0"
|
||||
}
|
||||
|
||||
if len(aDigits) != len(bDigits) {
|
||||
return cmp.Compare(len(aDigits), len(bDigits))
|
||||
}
|
||||
if cmp := strings.Compare(aDigits, bDigits); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
func compareOrganizeImportsCaseUpperFirst(a string, b string) int {
|
||||
return compareOrganizeImportsCase(a, b, OrganizeImportsCaseFirstUpper)
|
||||
}
|
||||
|
||||
func compareOrganizeImportsCase(a string, b string, caseFirst OrganizeImportsCaseFirst) int {
|
||||
aRunes := []rune(a)
|
||||
bRunes := []rune(b)
|
||||
minLen := min(len(aRunes), len(bRunes))
|
||||
|
||||
for i := range minLen {
|
||||
aUpper := unicode.IsUpper(aRunes[i])
|
||||
bUpper := unicode.IsUpper(bRunes[i])
|
||||
if aUpper != bUpper {
|
||||
switch caseFirst {
|
||||
case OrganizeImportsCaseFirstUpper:
|
||||
if aUpper {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
case OrganizeImportsCaseFirstLower:
|
||||
if !aUpper {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
default:
|
||||
if aUpper {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cmp.Compare(len(aRunes), len(bRunes))
|
||||
}
|
||||
|
||||
func getOrganizeImportsPresetStringComparer(sort OrganizeImportsSort) func(a, b string) int {
|
||||
switch sort {
|
||||
case OrganizeImportsSortOrdinalIgnoreCase:
|
||||
return getOrganizeImportsOrdinalStringComparer(true)
|
||||
case OrganizeImportsSortNatural:
|
||||
return getOrganizeImportsNaturalStringComparer(true)
|
||||
case OrganizeImportsSortNaturalIgnoreCase:
|
||||
return getOrganizeImportsNaturalStringComparer(false)
|
||||
default:
|
||||
return getOrganizeImportsOrdinalStringComparer(false)
|
||||
}
|
||||
}
|
||||
|
||||
func getOrganizeImportsStringComparer(preferences UserPreferences, ignoreCase bool) func(a, b string) int {
|
||||
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto {
|
||||
return getOrganizeImportsPresetStringComparer(preferences.OrganizeImportsSort)
|
||||
}
|
||||
if preferences.OrganizeImportsCollation == OrganizeImportsCollationUnicode {
|
||||
return getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences)
|
||||
}
|
||||
return getOrganizeImportsOrdinalStringComparer(ignoreCase)
|
||||
}
|
||||
|
||||
func getModuleSpecifierExpression(declaration *ast.Statement) *ast.Expression {
|
||||
switch declaration.Kind {
|
||||
case ast.KindImportEqualsDeclaration:
|
||||
importEquals := declaration.AsImportEqualsDeclaration()
|
||||
if importEquals.ModuleReference.Kind == ast.KindExternalModuleReference {
|
||||
return importEquals.ModuleReference.Expression()
|
||||
}
|
||||
return nil
|
||||
case ast.KindImportDeclaration:
|
||||
return declaration.ModuleSpecifier()
|
||||
case ast.KindVariableStatement:
|
||||
declarations := declaration.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes
|
||||
if len(declarations) > 0 {
|
||||
initializer := declarations[0].Initializer()
|
||||
if initializer != nil && initializer.Kind == ast.KindCallExpression {
|
||||
callExpr := initializer.AsCallExpression()
|
||||
if len(callExpr.Arguments.Nodes) > 0 {
|
||||
return callExpr.Arguments.Nodes[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetExternalModuleName returns the module name from a module specifier expression.
|
||||
func GetExternalModuleName(specifier *ast.Expression) string {
|
||||
if specifier != nil && ast.IsStringLiteralLike(specifier.AsNode()) {
|
||||
return specifier.Text()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CompareModuleSpecifiers compares two module specifiers using the given comparer.
|
||||
func CompareModuleSpecifiers(m1 *ast.Expression, m2 *ast.Expression, comparer func(a, b string) int) int {
|
||||
name1 := GetExternalModuleName(m1)
|
||||
name2 := GetExternalModuleName(m2)
|
||||
if cmp := core.CompareBooleans(name1 == "", name2 == ""); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
if cmp := core.CompareBooleans(tspath.IsExternalModuleNameRelative(name1), tspath.IsExternalModuleNameRelative(name2)); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return comparer(name1, name2)
|
||||
}
|
||||
|
||||
func compareImportKind(s1 *ast.Statement, s2 *ast.Statement) int {
|
||||
return cmp.Compare(getImportKindOrder(s1), getImportKindOrder(s2))
|
||||
}
|
||||
|
||||
// getImportKindOrder returns the sort order for different import kinds:
|
||||
// 1. Side-effect imports
|
||||
// 2. Type-only imports
|
||||
// 3. Namespace imports
|
||||
// 4. Default imports
|
||||
// 5. Named imports
|
||||
// 6. ImportEqualsDeclarations
|
||||
// 7. Require variable statements
|
||||
const (
|
||||
importKindOrderSideEffect = 0
|
||||
importKindOrderTypeOnly = 1
|
||||
importKindOrderNamespace = 2
|
||||
importKindOrderDefault = 3
|
||||
importKindOrderNamed = 4
|
||||
importKindOrderImportEquals = 5
|
||||
importKindOrderRequire = 6
|
||||
importKindOrderUnknown = 7
|
||||
)
|
||||
|
||||
func getImportKindOrder(s1 *ast.Statement) int {
|
||||
switch s1.Kind {
|
||||
case ast.KindImportDeclaration:
|
||||
importDecl := s1.AsImportDeclaration()
|
||||
if importDecl.ImportClause == nil {
|
||||
return importKindOrderSideEffect
|
||||
}
|
||||
importClause := importDecl.ImportClause.AsImportClause()
|
||||
if importClause.IsTypeOnly() {
|
||||
return importKindOrderTypeOnly
|
||||
}
|
||||
if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamespaceImport {
|
||||
return importKindOrderNamespace
|
||||
}
|
||||
if importClause.Name() != nil {
|
||||
return importKindOrderDefault
|
||||
}
|
||||
return importKindOrderNamed
|
||||
case ast.KindImportEqualsDeclaration:
|
||||
return importKindOrderImportEquals
|
||||
case ast.KindVariableStatement:
|
||||
return importKindOrderRequire
|
||||
default:
|
||||
return importKindOrderUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// CompareImportsOrRequireStatements compares two import or require statements.
|
||||
func CompareImportsOrRequireStatements(s1 *ast.Statement, s2 *ast.Statement, comparer func(a, b string) int) int {
|
||||
if cmp := CompareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return compareImportKind(s1, s2)
|
||||
}
|
||||
|
||||
func compareImportOrExportSpecifiers(s1 *ast.Node, s2 *ast.Node, comparer func(a, b string) int, preferences UserPreferences) int {
|
||||
typeOrder := preferences.OrganizeImportsTypeOrder
|
||||
|
||||
s1Name := s1.Name().Text()
|
||||
s2Name := s2.Name().Text()
|
||||
|
||||
switch typeOrder {
|
||||
case OrganizeImportsTypeOrderFirst:
|
||||
if cmp := core.CompareBooleans(s2.IsTypeOnly(), s1.IsTypeOnly()); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return comparer(s1Name, s2Name)
|
||||
case OrganizeImportsTypeOrderInline:
|
||||
return comparer(s1Name, s2Name)
|
||||
default: // OrganizeImportsTypeOrderLast
|
||||
if cmp := core.CompareBooleans(s1.IsTypeOnly(), s2.IsTypeOnly()); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return comparer(s1Name, s2Name)
|
||||
}
|
||||
}
|
||||
|
||||
// GetNamedImportSpecifierComparer returns a comparer function for sorting import specifiers.
|
||||
func GetNamedImportSpecifierComparer(preferences UserPreferences, comparer func(a, b string) int) func(s1, s2 *ast.Node) int {
|
||||
if comparer == nil {
|
||||
ignoreCase := false
|
||||
if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
|
||||
ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue()
|
||||
}
|
||||
comparer = getOrganizeImportsStringComparer(preferences, ignoreCase)
|
||||
}
|
||||
return func(s1, s2 *ast.Node) int {
|
||||
return compareImportOrExportSpecifiers(s1, s2, comparer, preferences)
|
||||
}
|
||||
}
|
||||
|
||||
// GetImportSpecifierInsertionIndex returns the index at which to insert a new import specifier.
|
||||
func GetImportSpecifierInsertionIndex(sortedImports []*ast.Node, newImport *ast.Node, comparer func(s1, s2 *ast.Node) int) int {
|
||||
return core.FirstResult(core.BinarySearchUniqueFunc(sortedImports, func(mid int, value *ast.Node) int {
|
||||
return comparer(value, newImport)
|
||||
}))
|
||||
}
|
||||
|
||||
// GetImportDeclarationInsertIndex returns the index at which to insert a new import declaration.
|
||||
func GetImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int {
|
||||
return core.FirstResult(core.BinarySearchUniqueFunc(sortedImports, func(mid int, value *ast.Statement) int {
|
||||
return comparer(value, newImport)
|
||||
}))
|
||||
}
|
||||
|
||||
// GetOrganizeImportsStringComparerWithDetection returns a string comparer based on detecting the order of import statements by the module specifier
|
||||
func GetOrganizeImportsStringComparerWithDetection(originalImportDecls []*ast.Statement, preferences UserPreferences) (comparer func(a, b string) int, isSorted bool) {
|
||||
result, sorted := DetectModuleSpecifierCaseBySort([][]*ast.Statement{originalImportDecls}, getComparers(preferences))
|
||||
return result, sorted
|
||||
}
|
||||
|
||||
func getComparers(preferences UserPreferences) []func(a string, b string) int {
|
||||
if preferences.OrganizeImportsSort != OrganizeImportsSortAuto || !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
|
||||
ignoreCase := false
|
||||
if !preferences.OrganizeImportsIgnoreCase.IsUnknown() {
|
||||
ignoreCase = preferences.OrganizeImportsIgnoreCase.IsTrue()
|
||||
}
|
||||
return []func(a, b string) int{getOrganizeImportsStringComparer(preferences, ignoreCase)}
|
||||
}
|
||||
return []func(a, b string) int{
|
||||
getOrganizeImportsStringComparer(preferences, true),
|
||||
getOrganizeImportsStringComparer(preferences, false),
|
||||
}
|
||||
}
|
||||
|
||||
type namedImportSortResult struct {
|
||||
namedImportComparer func(a, b string) int
|
||||
typeOrder OrganizeImportsTypeOrder
|
||||
isSorted bool
|
||||
}
|
||||
|
||||
// DetectNamedImportOrganizationBySort detects the order of named imports throughout the file by considering the named imports in each statement as a group
|
||||
func DetectNamedImportOrganizationBySort(
|
||||
originalGroups []*ast.Statement,
|
||||
comparersToTest []func(a, b string) int,
|
||||
typesToTest []OrganizeImportsTypeOrder,
|
||||
) (comparer func(a, b string) int, typeOrder OrganizeImportsTypeOrder, found bool) {
|
||||
result := detectNamedImportOrganizationBySort(originalGroups, comparersToTest, typesToTest)
|
||||
if result == nil {
|
||||
return nil, OrganizeImportsTypeOrderLast, false
|
||||
}
|
||||
return result.namedImportComparer, result.typeOrder, true
|
||||
}
|
||||
|
||||
func detectNamedImportOrganizationBySort(
|
||||
originalGroups []*ast.Statement,
|
||||
comparersToTest []func(a, b string) int,
|
||||
typesToTest []OrganizeImportsTypeOrder,
|
||||
) *namedImportSortResult {
|
||||
var bothNamedImports bool
|
||||
var importDeclsWithNamed []*ast.Statement
|
||||
|
||||
for _, imp := range originalGroups {
|
||||
if imp.AsImportDeclaration().ImportClause == nil {
|
||||
continue
|
||||
}
|
||||
clause := imp.AsImportDeclaration().ImportClause.AsImportClause()
|
||||
if clause.NamedBindings == nil || clause.NamedBindings.Kind != ast.KindNamedImports {
|
||||
continue
|
||||
}
|
||||
namedImports := clause.NamedBindings.AsNamedImports()
|
||||
if len(namedImports.Elements.Nodes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !bothNamedImports {
|
||||
hasTypeOnly := false
|
||||
hasRegular := false
|
||||
for _, elem := range namedImports.Elements.Nodes {
|
||||
if elem.IsTypeOnly() {
|
||||
hasTypeOnly = true
|
||||
} else {
|
||||
hasRegular = true
|
||||
}
|
||||
}
|
||||
if hasTypeOnly && hasRegular {
|
||||
bothNamedImports = true
|
||||
}
|
||||
}
|
||||
|
||||
importDeclsWithNamed = append(importDeclsWithNamed, imp)
|
||||
}
|
||||
|
||||
if len(importDeclsWithNamed) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
namedImportsByDecl := make([][]*ast.Statement, 0, len(importDeclsWithNamed))
|
||||
for _, imp := range importDeclsWithNamed {
|
||||
clause := imp.AsImportDeclaration().ImportClause.AsImportClause()
|
||||
namedImports := clause.NamedBindings.AsNamedImports()
|
||||
namedImportsByDecl = append(namedImportsByDecl, namedImports.Elements.Nodes)
|
||||
}
|
||||
|
||||
if !bothNamedImports || len(typesToTest) == 0 {
|
||||
namesList := make([][]string, len(namedImportsByDecl))
|
||||
for i, imports := range namedImportsByDecl {
|
||||
names := make([]string, len(imports))
|
||||
for j, imp := range imports {
|
||||
names[j] = imp.Name().Text()
|
||||
}
|
||||
namesList[i] = names
|
||||
}
|
||||
sortState := detectCaseSensitivityBySort(namesList, comparersToTest)
|
||||
typeOrder := OrganizeImportsTypeOrderLast
|
||||
if len(typesToTest) == 1 {
|
||||
typeOrder = typesToTest[0]
|
||||
}
|
||||
return &namedImportSortResult{
|
||||
namedImportComparer: sortState.comparer,
|
||||
typeOrder: typeOrder,
|
||||
isSorted: sortState.isSorted,
|
||||
}
|
||||
}
|
||||
|
||||
bestDiff := map[OrganizeImportsTypeOrder]int{
|
||||
OrganizeImportsTypeOrderFirst: math.MaxInt,
|
||||
OrganizeImportsTypeOrderLast: math.MaxInt,
|
||||
OrganizeImportsTypeOrderInline: math.MaxInt,
|
||||
}
|
||||
bestComparer := map[OrganizeImportsTypeOrder]func(a, b string) int{
|
||||
OrganizeImportsTypeOrderFirst: comparersToTest[0],
|
||||
OrganizeImportsTypeOrderLast: comparersToTest[0],
|
||||
OrganizeImportsTypeOrderInline: comparersToTest[0],
|
||||
}
|
||||
|
||||
for _, curComparer := range comparersToTest {
|
||||
currDiff := map[OrganizeImportsTypeOrder]int{
|
||||
OrganizeImportsTypeOrderFirst: 0,
|
||||
OrganizeImportsTypeOrderLast: 0,
|
||||
OrganizeImportsTypeOrderInline: 0,
|
||||
}
|
||||
|
||||
for _, importDecl := range namedImportsByDecl {
|
||||
for _, typeOrder := range typesToTest {
|
||||
prefs := UserPreferences{OrganizeImportsTypeOrder: typeOrder}
|
||||
diff := measureSortedness(importDecl, func(n1, n2 *ast.Node) int {
|
||||
return compareImportOrExportSpecifiers(n1, n2, curComparer, prefs)
|
||||
})
|
||||
currDiff[typeOrder] = currDiff[typeOrder] + diff
|
||||
}
|
||||
}
|
||||
|
||||
for _, typeOrder := range typesToTest {
|
||||
if currDiff[typeOrder] < bestDiff[typeOrder] {
|
||||
bestDiff[typeOrder] = currDiff[typeOrder]
|
||||
bestComparer[typeOrder] = curComparer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, bestTypeOrder := range typesToTest {
|
||||
isBest := true
|
||||
for _, testTypeOrder := range typesToTest {
|
||||
if bestDiff[testTypeOrder] < bestDiff[bestTypeOrder] {
|
||||
isBest = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isBest {
|
||||
return &namedImportSortResult{
|
||||
namedImportComparer: bestComparer[bestTypeOrder],
|
||||
typeOrder: bestTypeOrder,
|
||||
isSorted: bestDiff[bestTypeOrder] == 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &namedImportSortResult{
|
||||
namedImportComparer: bestComparer[OrganizeImportsTypeOrderLast],
|
||||
typeOrder: OrganizeImportsTypeOrderLast,
|
||||
isSorted: bestDiff[OrganizeImportsTypeOrderLast] == 0,
|
||||
}
|
||||
}
|
||||
|
||||
type caseSensitivityDetectionResult struct {
|
||||
comparer func(a, b string) int
|
||||
isSorted bool
|
||||
}
|
||||
|
||||
// DetectModuleSpecifierCaseBySort detects the order of module specifiers based on import statements throughout the module/file
|
||||
func DetectModuleSpecifierCaseBySort(importDeclsByGroup [][]*ast.Statement, comparersToTest []func(a, b string) int) (comparer func(a, b string) int, isSorted bool) {
|
||||
moduleSpecifiersByGroup := make([][]string, 0, len(importDeclsByGroup))
|
||||
for _, importGroup := range importDeclsByGroup {
|
||||
moduleNames := make([]string, 0, len(importGroup))
|
||||
for _, decl := range importGroup {
|
||||
if expr := getModuleSpecifierExpression(decl); expr != nil {
|
||||
moduleNames = append(moduleNames, GetExternalModuleName(expr))
|
||||
} else {
|
||||
moduleNames = append(moduleNames, "")
|
||||
}
|
||||
}
|
||||
moduleSpecifiersByGroup = append(moduleSpecifiersByGroup, moduleNames)
|
||||
}
|
||||
result := detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest)
|
||||
return result.comparer, result.isSorted
|
||||
}
|
||||
|
||||
func detectCaseSensitivityBySort(originalGroups [][]string, comparersToTest []func(a, b string) int) caseSensitivityDetectionResult {
|
||||
var bestComparer func(a, b string) int
|
||||
bestDiff := math.MaxInt
|
||||
|
||||
for _, curComparer := range comparersToTest {
|
||||
diffOfCurrentComparer := 0
|
||||
|
||||
for _, listToSort := range originalGroups {
|
||||
if len(listToSort) <= 1 {
|
||||
continue
|
||||
}
|
||||
diff := measureSortedness(listToSort, curComparer)
|
||||
diffOfCurrentComparer += diff
|
||||
}
|
||||
|
||||
if diffOfCurrentComparer < bestDiff {
|
||||
bestDiff = diffOfCurrentComparer
|
||||
bestComparer = curComparer
|
||||
}
|
||||
}
|
||||
|
||||
if bestComparer == nil && len(comparersToTest) > 0 {
|
||||
bestComparer = comparersToTest[0]
|
||||
}
|
||||
|
||||
return caseSensitivityDetectionResult{
|
||||
comparer: bestComparer,
|
||||
isSorted: bestDiff == 0,
|
||||
}
|
||||
}
|
||||
|
||||
func measureSortedness[T any](arr []T, comparer func(a, b T) int) int {
|
||||
i := 0
|
||||
for j := range len(arr) - 1 {
|
||||
if comparer(arr[j], arr[j+1]) > 0 {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// GetNamedImportSpecifierComparerWithDetection returns a specifier comparer based on detecting the existing sort order within a single import statement
|
||||
func GetNamedImportSpecifierComparerWithDetection(importDecl *ast.Node, sourceFile *ast.SourceFile, preferences UserPreferences) (specifierComparer func(s1, s2 *ast.Node) int, isSorted core.Tristate) {
|
||||
comparersToTest, typeOrdersToTest := GetDetectionLists(preferences)
|
||||
|
||||
var importStmt *ast.Statement
|
||||
if importDecl.Kind == ast.KindImportDeclaration {
|
||||
importStmt = importDecl
|
||||
}
|
||||
|
||||
specifierComparer = GetNamedImportSpecifierComparer(preferences, comparersToTest[0])
|
||||
isSorted = core.TSUnknown
|
||||
|
||||
if (ResolveOrganizeImportsSort(preferences) == OrganizeImportsSortAuto || preferences.OrganizeImportsTypeOrder == OrganizeImportsTypeOrderAuto) && importStmt != nil {
|
||||
detectFromDecl := detectNamedImportOrganizationBySort([]*ast.Statement{importStmt}, comparersToTest, typeOrdersToTest)
|
||||
if detectFromDecl != nil {
|
||||
isSorted = core.BoolToTristate(detectFromDecl.isSorted)
|
||||
specifierComparer = GetNamedImportSpecifierComparer(
|
||||
UserPreferences{OrganizeImportsTypeOrder: detectFromDecl.typeOrder},
|
||||
detectFromDecl.namedImportComparer,
|
||||
)
|
||||
} else if sourceFile != nil {
|
||||
allImports := FilterImportDeclarations(sourceFile.Statements.Nodes)
|
||||
detectFromFile := detectNamedImportOrganizationBySort(allImports, comparersToTest, typeOrdersToTest)
|
||||
if detectFromFile != nil {
|
||||
isSorted = core.BoolToTristate(detectFromFile.isSorted)
|
||||
specifierComparer = GetNamedImportSpecifierComparer(
|
||||
UserPreferences{OrganizeImportsTypeOrder: detectFromFile.typeOrder},
|
||||
detectFromFile.namedImportComparer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return specifierComparer, isSorted
|
||||
}
|
||||
438
tools/tsgo/internal/ls/lsutil/symbol_display.go
Normal file
438
tools/tsgo/internal/ls/lsutil/symbol_display.go
Normal file
@@ -0,0 +1,438 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
)
|
||||
|
||||
type ScriptElementKind int
|
||||
|
||||
const (
|
||||
ScriptElementKindUnknown ScriptElementKind = iota
|
||||
ScriptElementKindWarning
|
||||
// predefined type (void) or keyword (class)
|
||||
ScriptElementKindKeyword
|
||||
// top level script node
|
||||
ScriptElementKindScriptElement
|
||||
// module foo {}
|
||||
ScriptElementKindModuleElement
|
||||
// class X {}
|
||||
ScriptElementKindClassElement
|
||||
// var x = class X {}
|
||||
ScriptElementKindLocalClassElement
|
||||
// interface Y {}
|
||||
ScriptElementKindInterfaceElement
|
||||
// type T = ...
|
||||
ScriptElementKindTypeElement
|
||||
// enum E {}
|
||||
ScriptElementKindEnumElement
|
||||
ScriptElementKindEnumMemberElement
|
||||
// Inside module and script only.
|
||||
// const v = ...
|
||||
ScriptElementKindVariableElement
|
||||
// Inside function.
|
||||
ScriptElementKindLocalVariableElement
|
||||
// using foo = ...
|
||||
ScriptElementKindVariableUsingElement
|
||||
// await using foo = ...
|
||||
ScriptElementKindVariableAwaitUsingElement
|
||||
// Inside module and script only.
|
||||
// function f() {}
|
||||
ScriptElementKindFunctionElement
|
||||
// Inside function.
|
||||
ScriptElementKindLocalFunctionElement
|
||||
// class X { [public|private]* foo() {} }
|
||||
ScriptElementKindMemberFunctionElement
|
||||
// class X { [public|private]* [get|set] foo:number; }
|
||||
ScriptElementKindMemberGetAccessorElement
|
||||
ScriptElementKindMemberSetAccessorElement
|
||||
// class X { [public|private]* foo:number; }
|
||||
// interface Y { foo:number; }
|
||||
ScriptElementKindMemberVariableElement
|
||||
// class X { [public|private]* accessor foo: number; }
|
||||
ScriptElementKindMemberAccessorVariableElement
|
||||
// class X { constructor() { } }
|
||||
// class X { static { } }
|
||||
ScriptElementKindConstructorImplementationElement
|
||||
// interface Y { ():number; }
|
||||
ScriptElementKindCallSignatureElement
|
||||
// interface Y { []:number; }
|
||||
ScriptElementKindIndexSignatureElement
|
||||
// interface Y { new():Y; }
|
||||
ScriptElementKindConstructSignatureElement
|
||||
// function foo(*Y*: string)
|
||||
ScriptElementKindParameterElement
|
||||
ScriptElementKindTypeParameterElement
|
||||
ScriptElementKindPrimitiveType
|
||||
ScriptElementKindLabel
|
||||
ScriptElementKindAlias
|
||||
ScriptElementKindConstElement
|
||||
ScriptElementKindLetElement
|
||||
ScriptElementKindDirectory
|
||||
ScriptElementKindExternalModuleName
|
||||
// String literal
|
||||
ScriptElementKindString
|
||||
// Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}"
|
||||
ScriptElementKindLink
|
||||
// Jsdoc @link: in `{@link C link text}`, the entity name "C"
|
||||
ScriptElementKindLinkName
|
||||
// Jsdoc @link: in `{@link C link text}`, the link text "link text"
|
||||
ScriptElementKindLinkText
|
||||
)
|
||||
|
||||
type ScriptElementKindModifier uint32
|
||||
|
||||
const (
|
||||
ScriptElementKindModifierNone ScriptElementKindModifier = 0
|
||||
ScriptElementKindModifierPublic ScriptElementKindModifier = 1 << iota
|
||||
ScriptElementKindModifierPrivate
|
||||
ScriptElementKindModifierProtected
|
||||
ScriptElementKindModifierExported
|
||||
ScriptElementKindModifierAmbient
|
||||
ScriptElementKindModifierStatic
|
||||
ScriptElementKindModifierAbstract
|
||||
ScriptElementKindModifierOptional
|
||||
ScriptElementKindModifierDeprecated
|
||||
ScriptElementKindModifierDts
|
||||
ScriptElementKindModifierTs
|
||||
ScriptElementKindModifierTsx
|
||||
ScriptElementKindModifierJs
|
||||
ScriptElementKindModifierJsx
|
||||
ScriptElementKindModifierJson
|
||||
ScriptElementKindModifierDmts
|
||||
ScriptElementKindModifierMts
|
||||
ScriptElementKindModifierMjs
|
||||
ScriptElementKindModifierDcts
|
||||
ScriptElementKindModifierCts
|
||||
ScriptElementKindModifierCjs
|
||||
)
|
||||
|
||||
var scriptElementKindModifierNames = []struct {
|
||||
flag ScriptElementKindModifier
|
||||
name string
|
||||
}{
|
||||
{ScriptElementKindModifierPublic, "public"},
|
||||
{ScriptElementKindModifierPrivate, "private"},
|
||||
{ScriptElementKindModifierProtected, "protected"},
|
||||
{ScriptElementKindModifierExported, "export"},
|
||||
{ScriptElementKindModifierAmbient, "declare"},
|
||||
{ScriptElementKindModifierStatic, "static"},
|
||||
{ScriptElementKindModifierAbstract, "abstract"},
|
||||
{ScriptElementKindModifierOptional, "optional"},
|
||||
{ScriptElementKindModifierDeprecated, "deprecated"},
|
||||
{ScriptElementKindModifierDts, ".d.ts"},
|
||||
{ScriptElementKindModifierTs, ".ts"},
|
||||
{ScriptElementKindModifierTsx, ".tsx"},
|
||||
{ScriptElementKindModifierJs, ".js"},
|
||||
{ScriptElementKindModifierJsx, ".jsx"},
|
||||
{ScriptElementKindModifierJson, ".json"},
|
||||
{ScriptElementKindModifierDmts, ".d.mts"},
|
||||
{ScriptElementKindModifierMts, ".mts"},
|
||||
{ScriptElementKindModifierMjs, ".mjs"},
|
||||
{ScriptElementKindModifierDcts, ".d.cts"},
|
||||
{ScriptElementKindModifierCts, ".cts"},
|
||||
{ScriptElementKindModifierCjs, ".cjs"},
|
||||
}
|
||||
|
||||
func (m ScriptElementKindModifier) Strings() collections.Set[string] {
|
||||
result := collections.Set[string]{}
|
||||
for _, entry := range scriptElementKindModifierNames {
|
||||
if m&entry.flag != 0 {
|
||||
result.Add(entry.name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var FileExtensionKindModifiers = ScriptElementKindModifierDts |
|
||||
ScriptElementKindModifierTs |
|
||||
ScriptElementKindModifierTsx |
|
||||
ScriptElementKindModifierJs |
|
||||
ScriptElementKindModifierJsx |
|
||||
ScriptElementKindModifierJson |
|
||||
ScriptElementKindModifierDmts |
|
||||
ScriptElementKindModifierMts |
|
||||
ScriptElementKindModifierMjs |
|
||||
ScriptElementKindModifierDcts |
|
||||
ScriptElementKindModifierCts |
|
||||
ScriptElementKindModifierCjs
|
||||
|
||||
func GetSymbolKind(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind {
|
||||
result := getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location)
|
||||
if result != ScriptElementKindUnknown {
|
||||
return result
|
||||
}
|
||||
flags := symbol.CombinedLocalAndExportSymbolFlags()
|
||||
if flags&ast.SymbolFlagsClass != 0 {
|
||||
decl := ast.GetDeclarationOfKind(symbol, ast.KindClassExpression)
|
||||
if decl != nil {
|
||||
return ScriptElementKindLocalClassElement
|
||||
}
|
||||
return ScriptElementKindClassElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsEnum != 0 {
|
||||
return ScriptElementKindEnumElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsTypeAlias != 0 {
|
||||
return ScriptElementKindTypeElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsInterface != 0 {
|
||||
return ScriptElementKindInterfaceElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsTypeParameter != 0 {
|
||||
return ScriptElementKindTypeParameterElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsEnumMember != 0 {
|
||||
return ScriptElementKindEnumMemberElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsAlias != 0 {
|
||||
return ScriptElementKindAlias
|
||||
}
|
||||
if flags&ast.SymbolFlagsModule != 0 {
|
||||
return ScriptElementKindModuleElement
|
||||
}
|
||||
|
||||
return ScriptElementKindUnknown
|
||||
}
|
||||
|
||||
func getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker *checker.Checker, symbol *ast.Symbol, location *ast.Node) ScriptElementKind {
|
||||
var roots []*ast.Symbol
|
||||
if typeChecker != nil {
|
||||
roots = typeChecker.GetRootSymbols(symbol)
|
||||
} else {
|
||||
roots = []*ast.Symbol{symbol}
|
||||
}
|
||||
|
||||
// If this is a method from a mapped type, leave as a method so long as it still has a call signature, as opposed to e.g.
|
||||
// `{ [K in keyof I]: number }`.
|
||||
if len(roots) == 1 &&
|
||||
roots[0].Flags&ast.SymbolFlagsMethod != 0 &&
|
||||
(typeChecker == nil || len(typeChecker.GetCallSignatures(typeChecker.GetNonNullableType(typeChecker.GetTypeOfSymbolAtLocation(symbol, location)))) > 0) {
|
||||
return ScriptElementKindMemberFunctionElement
|
||||
}
|
||||
|
||||
if typeChecker != nil {
|
||||
if typeChecker.IsUndefinedSymbol(symbol) {
|
||||
return ScriptElementKindVariableElement
|
||||
}
|
||||
if typeChecker.IsArgumentsSymbol(symbol) {
|
||||
return ScriptElementKindLocalVariableElement
|
||||
}
|
||||
if location.Kind == ast.KindThisKeyword && ast.IsExpression(location) ||
|
||||
ast.IsThisInTypeQuery(location) {
|
||||
return ScriptElementKindParameterElement
|
||||
}
|
||||
}
|
||||
|
||||
flags := symbol.CombinedLocalAndExportSymbolFlags()
|
||||
if flags&ast.SymbolFlagsVariable != 0 {
|
||||
if isFirstDeclarationOfSymbolParameter(symbol) {
|
||||
return ScriptElementKindParameterElement
|
||||
} else if symbol.ValueDeclaration != nil && ast.IsVarConst(symbol.ValueDeclaration) {
|
||||
return ScriptElementKindConstElement
|
||||
} else if symbol.ValueDeclaration != nil && ast.IsVarUsing(symbol.ValueDeclaration) {
|
||||
return ScriptElementKindVariableUsingElement
|
||||
} else if symbol.ValueDeclaration != nil && ast.IsVarAwaitUsing(symbol.ValueDeclaration) {
|
||||
return ScriptElementKindVariableAwaitUsingElement
|
||||
} else if core.Some(symbol.Declarations, ast.IsLet) {
|
||||
return ScriptElementKindLetElement
|
||||
}
|
||||
if isLocalVariableOrFunction(symbol) {
|
||||
return ScriptElementKindLocalVariableElement
|
||||
}
|
||||
return ScriptElementKindVariableElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsFunction != 0 {
|
||||
if isLocalVariableOrFunction(symbol) {
|
||||
return ScriptElementKindLocalFunctionElement
|
||||
}
|
||||
return ScriptElementKindFunctionElement
|
||||
}
|
||||
// FIXME: getter and setter use the same symbol. And it is rare to use only setter without getter, so in most cases the symbol always has getter flag.
|
||||
// So, even when the location is just on the declaration of setter, this function returns getter.
|
||||
if flags&ast.SymbolFlagsGetAccessor != 0 {
|
||||
return ScriptElementKindMemberGetAccessorElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsSetAccessor != 0 {
|
||||
return ScriptElementKindMemberSetAccessorElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsMethod != 0 {
|
||||
return ScriptElementKindMemberFunctionElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsConstructor != 0 {
|
||||
return ScriptElementKindConstructorImplementationElement
|
||||
}
|
||||
if flags&ast.SymbolFlagsSignature != 0 {
|
||||
return ScriptElementKindIndexSignatureElement
|
||||
}
|
||||
|
||||
if flags&ast.SymbolFlagsProperty != 0 {
|
||||
if typeChecker != nil && flags&ast.SymbolFlagsTransient != 0 && symbol.CheckFlags&ast.CheckFlagsSynthetic != 0 {
|
||||
// If union property is result of union of non method (property/accessors/variables), it is labeled as property
|
||||
var unionPropertyKind ScriptElementKind
|
||||
for _, rootSymbol := range roots {
|
||||
if rootSymbol.Flags&(ast.SymbolFlagsPropertyOrAccessor|ast.SymbolFlagsVariable) != 0 {
|
||||
unionPropertyKind = ScriptElementKindMemberVariableElement
|
||||
break
|
||||
}
|
||||
}
|
||||
if unionPropertyKind == ScriptElementKindUnknown {
|
||||
// If this was union of all methods,
|
||||
// make sure it has call signatures before we can label it as method.
|
||||
typeOfUnionProperty := typeChecker.GetTypeOfSymbolAtLocation(symbol, location)
|
||||
if len(typeChecker.GetCallSignatures(typeOfUnionProperty)) > 0 {
|
||||
return ScriptElementKindMemberFunctionElement
|
||||
}
|
||||
return ScriptElementKindMemberVariableElement
|
||||
}
|
||||
return unionPropertyKind
|
||||
}
|
||||
|
||||
return ScriptElementKindMemberVariableElement
|
||||
}
|
||||
|
||||
return ScriptElementKindUnknown
|
||||
}
|
||||
|
||||
func isFirstDeclarationOfSymbolParameter(symbol *ast.Symbol) bool {
|
||||
var declaration *ast.Node
|
||||
if len(symbol.Declarations) > 0 {
|
||||
declaration = symbol.Declarations[0]
|
||||
}
|
||||
result := ast.FindAncestorOrQuit(declaration, func(n *ast.Node) ast.FindAncestorResult {
|
||||
if ast.IsParameterDeclaration(n) {
|
||||
return ast.FindAncestorTrue
|
||||
}
|
||||
if ast.IsBindingElement(n) || ast.IsObjectBindingPattern(n) || ast.IsArrayBindingPattern(n) {
|
||||
return ast.FindAncestorFalse
|
||||
}
|
||||
return ast.FindAncestorQuit
|
||||
})
|
||||
|
||||
return result != nil
|
||||
}
|
||||
|
||||
func isLocalVariableOrFunction(symbol *ast.Symbol) bool {
|
||||
if symbol.Parent != nil {
|
||||
return false // This is exported symbol
|
||||
}
|
||||
|
||||
for _, decl := range symbol.Declarations {
|
||||
// Function expressions are local
|
||||
if decl.Kind == ast.KindFunctionExpression {
|
||||
return true
|
||||
}
|
||||
|
||||
if decl.Kind != ast.KindVariableDeclaration && decl.Kind != ast.KindFunctionDeclaration {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the parent is not source file or module block, it is a local variable.
|
||||
parent := decl.Parent
|
||||
for ; !ast.IsFunctionBlock(parent); parent = parent.Parent {
|
||||
// Reached source file or module block
|
||||
if parent.Kind == ast.KindSourceFile || parent.Kind == ast.KindModuleBlock {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ast.IsFunctionBlock(parent) {
|
||||
// Parent is in function block.
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) ScriptElementKindModifier {
|
||||
if symbol == nil {
|
||||
return ScriptElementKindModifierNone
|
||||
}
|
||||
|
||||
modifiers := getNormalizedSymbolModifiers(typeChecker, symbol)
|
||||
if symbol.Flags&ast.SymbolFlagsAlias != 0 && typeChecker != nil {
|
||||
resolvedSymbol := typeChecker.GetAliasedSymbol(symbol)
|
||||
if resolvedSymbol != symbol {
|
||||
modifiers |= getNormalizedSymbolModifiers(typeChecker, resolvedSymbol)
|
||||
}
|
||||
}
|
||||
if symbol.Flags&ast.SymbolFlagsOptional != 0 {
|
||||
modifiers |= ScriptElementKindModifierOptional
|
||||
}
|
||||
|
||||
return modifiers
|
||||
}
|
||||
|
||||
func getNormalizedSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) ScriptElementKindModifier {
|
||||
var modifierSet ScriptElementKindModifier
|
||||
if len(symbol.Declarations) > 0 {
|
||||
declaration := symbol.Declarations[0]
|
||||
declarations := symbol.Declarations[1:]
|
||||
// omit deprecated flag if some declarations are not deprecated
|
||||
var excludeFlags ast.ModifierFlags
|
||||
if len(declarations) > 0 &&
|
||||
isDeprecatedDeclaration(typeChecker, declaration) && // !!! include jsdoc node flags
|
||||
core.Some(declarations, func(d *ast.Node) bool { return !isDeprecatedDeclaration(typeChecker, d) }) {
|
||||
excludeFlags = ast.ModifierFlagsDeprecated
|
||||
} else {
|
||||
excludeFlags = ast.ModifierFlagsNone
|
||||
}
|
||||
modifierSet = getNodeModifiers(typeChecker, declaration, excludeFlags)
|
||||
}
|
||||
|
||||
return modifierSet
|
||||
}
|
||||
|
||||
func isDeprecatedDeclaration(typeChecker *checker.Checker, declaration *ast.Node) bool {
|
||||
if typeChecker != nil {
|
||||
return typeChecker.IsDeprecatedDeclaration(declaration)
|
||||
}
|
||||
return ast.IsDeprecatedDeclaration(declaration)
|
||||
}
|
||||
|
||||
func getNodeModifiers(typeChecker *checker.Checker, node *ast.Node, excludeFlags ast.ModifierFlags) ScriptElementKindModifier {
|
||||
var result ScriptElementKindModifier
|
||||
var flags ast.ModifierFlags
|
||||
if ast.IsDeclaration(node) {
|
||||
flags = ast.GetCombinedModifierFlags(node)
|
||||
if isDeprecatedDeclaration(typeChecker, node) {
|
||||
flags |= ast.ModifierFlagsDeprecated
|
||||
}
|
||||
flags &^= excludeFlags
|
||||
}
|
||||
|
||||
if flags&ast.ModifierFlagsPrivate != 0 {
|
||||
result |= ScriptElementKindModifierPrivate
|
||||
}
|
||||
if flags&ast.ModifierFlagsProtected != 0 {
|
||||
result |= ScriptElementKindModifierProtected
|
||||
}
|
||||
if flags&ast.ModifierFlagsPublic != 0 {
|
||||
result |= ScriptElementKindModifierPublic
|
||||
}
|
||||
if flags&ast.ModifierFlagsStatic != 0 {
|
||||
result |= ScriptElementKindModifierStatic
|
||||
}
|
||||
if flags&ast.ModifierFlagsAbstract != 0 {
|
||||
result |= ScriptElementKindModifierAbstract
|
||||
}
|
||||
if flags&ast.ModifierFlagsExport != 0 {
|
||||
result |= ScriptElementKindModifierExported
|
||||
}
|
||||
if flags&ast.ModifierFlagsDeprecated != 0 {
|
||||
result |= ScriptElementKindModifierDeprecated
|
||||
}
|
||||
if flags&ast.ModifierFlagsAmbient != 0 {
|
||||
result |= ScriptElementKindModifierAmbient
|
||||
}
|
||||
if node.Flags&ast.NodeFlagsAmbient != 0 {
|
||||
result |= ScriptElementKindModifierAmbient
|
||||
}
|
||||
if node.Kind == ast.KindExportAssignment {
|
||||
result |= ScriptElementKindModifierExported
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
901
tools/tsgo/internal/ls/lsutil/userpreferences.go
Normal file
901
tools/tsgo/internal/ls/lsutil/userpreferences.go
Normal file
@@ -0,0 +1,901 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/modulespecifiers"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
|
||||
)
|
||||
|
||||
func NewDefaultUserPreferences() UserPreferences {
|
||||
return UserPreferences{
|
||||
FormatCodeSettings: GetDefaultFormatCodeSettings(),
|
||||
|
||||
IncludeCompletionsForModuleExports: core.TSTrue,
|
||||
IncludeCompletionsForImportStatements: core.TSTrue,
|
||||
EnableAutoClosingTags: core.TSTrue,
|
||||
EnableJSDocCompletions: core.TSTrue,
|
||||
GenerateReturnInDocTemplate: core.TSTrue,
|
||||
|
||||
AllowRenameOfImportPath: core.TSTrue,
|
||||
ProvideRefactorNotApplicableReason: core.TSTrue,
|
||||
EnableFormatting: core.TSTrue,
|
||||
EnableValidation: core.TSTrue,
|
||||
DisplayPartsForJSDoc: core.TSTrue,
|
||||
DisableLineTextInReferences: core.TSTrue,
|
||||
ReportStyleChecksAsWarnings: core.TSTrue,
|
||||
|
||||
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
|
||||
}
|
||||
}
|
||||
|
||||
// UserPreferences represents TypeScript language service preferences.
|
||||
//
|
||||
// Fields are populated using two tags:
|
||||
// - `raw:"name"` or `raw:"name,invert"` - TypeScript/raw name for unstable section lookup
|
||||
// - `config:"path.to.setting"` or `config:"path.to.setting,invert"` - VS Code nested config path
|
||||
//
|
||||
// At least one tag must be present on each preference field.
|
||||
// The `,invert` modifier inverts boolean values (e.g., VS Code's "suppress" -> our "include").
|
||||
type UserPreferences struct {
|
||||
FormatCodeSettings FormatCodeSettings
|
||||
|
||||
QuotePreference QuotePreference `raw:"quotePreference" config:"preferences.quoteStyle"`
|
||||
LazyConfiguredProjectsFromExternalProject core.Tristate `raw:"lazyConfiguredProjectsFromExternalProject"` // !!!
|
||||
|
||||
// A positive integer indicating the maximum length of a hover text before it is truncated.
|
||||
//
|
||||
// Default: `500`
|
||||
MaximumHoverLength int `raw:"maximumHoverLength"` // !!!
|
||||
|
||||
// ------- Completions -------
|
||||
|
||||
// If enabled, TypeScript will search through all external modules' exports and add them to the completions list.
|
||||
// This affects lone identifier completions but not completions on the right hand side of `obj.`.
|
||||
IncludeCompletionsForModuleExports core.Tristate `raw:"includeCompletionsForModuleExports" config:"suggest.autoImports"`
|
||||
// Enables auto-import-style completions on partially-typed import statements. E.g., allows
|
||||
// `import write|` to be completed to `import { writeFile } from "fs"`.
|
||||
IncludeCompletionsForImportStatements core.Tristate `raw:"includeCompletionsForImportStatements" config:"suggest.includeCompletionsForImportStatements"`
|
||||
// Unless this option is `false`, member completion lists triggered with `.` will include entries
|
||||
// on potentially-null and potentially-undefined values, with insertion text to replace
|
||||
// preceding `.` tokens with `?.`.
|
||||
IncludeAutomaticOptionalChainCompletions core.Tristate `raw:"includeAutomaticOptionalChainCompletions" config:"suggest.includeAutomaticOptionalChainCompletions"`
|
||||
// If enabled, completions for class members (e.g. methods and properties) will include
|
||||
// a whole declaration for the member.
|
||||
// E.g., `class A { f| }` could be completed to `class A { foo(): number {} }`, instead of
|
||||
// `class A { foo }`.
|
||||
IncludeCompletionsWithClassMemberSnippets core.Tristate `raw:"includeCompletionsWithClassMemberSnippets" config:"suggest.classMemberSnippets.enabled"` // !!!
|
||||
// If enabled, object literal methods will have a method declaration completion entry in addition
|
||||
// to the regular completion entry containing just the method name.
|
||||
// E.g., `const objectLiteral: T = { f| }` could be completed to `const objectLiteral: T = { foo(): void {} }`,
|
||||
// in addition to `const objectLiteral: T = { foo }`.
|
||||
IncludeCompletionsWithObjectLiteralMethodSnippets core.Tristate `raw:"includeCompletionsWithObjectLiteralMethodSnippets" config:"suggest.objectLiteralMethodSnippets.enabled"` // !!!
|
||||
JsxAttributeCompletionStyle JsxAttributeCompletionStyle `raw:"jsxAttributeCompletionStyle" config:"preferences.jsxAttributeCompletionStyle"`
|
||||
EnableAutoClosingTags core.Tristate `raw:"autoClosingTags" config:"autoClosingTags.enabled" fallbackConfig:"autoClosingTags"`
|
||||
EnableJSDocCompletions core.Tristate `raw:"completeJSDocs" config:"suggest.jsdoc.enabled" fallbackConfig:"suggest.completeJSDocs"`
|
||||
GenerateReturnInDocTemplate core.Tristate `raw:"generateReturnInDocTemplate" config:"suggest.jsdoc.generateReturns"`
|
||||
|
||||
// ------- AutoImports --------
|
||||
|
||||
ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference `raw:"importModuleSpecifierPreference" config:"preferences.importModuleSpecifier"` // !!!
|
||||
// Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js"
|
||||
ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `raw:"importModuleSpecifierEnding" config:"preferences.importModuleSpecifierEnding"` // !!!
|
||||
AutoImportSpecifierExcludeRegexes []string `raw:"autoImportSpecifierExcludeRegexes" config:"preferences.autoImportSpecifierExcludeRegexes"` // !!!
|
||||
AutoImportFileExcludePatterns []string `raw:"autoImportFileExcludePatterns" config:"preferences.autoImportFileExcludePatterns"`
|
||||
AutoImportEntrypointDirectorySearch core.Tristate `raw:"autoImportEntrypointDirectorySearch" config:"preferences.autoImportEntrypointDirectorySearch"`
|
||||
PreferTypeOnlyAutoImports core.Tristate `raw:"preferTypeOnlyAutoImports" config:"preferences.preferTypeOnlyAutoImports"`
|
||||
|
||||
// ------- OrganizeImports -------
|
||||
|
||||
// Indicates which deterministic preset should be used to sort imports.
|
||||
// "auto" detects the existing ordinal case sensitivity where possible.
|
||||
OrganizeImportsSort OrganizeImportsSort `raw:"organizeImportsSort" config:"preferences.organizeImports.sort"` // !!!
|
||||
// Indicates whether imports should be organized in a case-insensitive manner.
|
||||
//
|
||||
// Default: TSUnknown ("auto" in strada), will perform detection
|
||||
OrganizeImportsIgnoreCase core.Tristate `raw:"organizeImportsIgnoreCase" config:"preferences.organizeImports.caseSensitivity"` // !!!
|
||||
// Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value of their
|
||||
// code points, or via "unicode" natural sorting. This implementation is locale-agnostic and approximates the practical
|
||||
// import-sorting behavior rather than the full Unicode Collation Algorithm.
|
||||
//
|
||||
// Default: Ordinal
|
||||
OrganizeImportsCollation OrganizeImportsCollation `raw:"organizeImportsCollation" config:"preferences.organizeImports.unicodeCollation"` // !!!
|
||||
// Indicates the locale to use for "unicode" collation in legacy clients. This is accepted for compatibility, but
|
||||
// currently ignored because organize-import sorting is deterministic and locale-agnostic.
|
||||
//
|
||||
// This preference is ignored if organizeImportsCollation is not `unicode`.
|
||||
//
|
||||
// Default: `"en"`
|
||||
OrganizeImportsLocale string `raw:"organizeImportsLocale" config:"preferences.organizeImports.locale"` // !!!
|
||||
// Indicates whether numeric collation should be used for digit sequences in strings. When `true`, will collate
|
||||
// strings such that `a1z < a2z < a100z`. When `false`, will collate strings such that `a1z < a100z < a2z`.
|
||||
//
|
||||
// This preference is ignored if organizeImportsCollation is not `unicode`.
|
||||
//
|
||||
// Default: `false`
|
||||
OrganizeImportsNumericCollation core.Tristate `raw:"organizeImportsNumericCollation" config:"preferences.organizeImports.numericCollation"` // !!!
|
||||
// Indicates whether accents and other diacritic marks are considered unequal for the purpose of sorting.
|
||||
//
|
||||
// This preference is ignored if organizeImportsCollation is not `unicode`.
|
||||
//
|
||||
// Default: `true`
|
||||
OrganizeImportsAccentCollation core.Tristate `raw:"organizeImportsAccentCollation" config:"preferences.organizeImports.accentCollation"` // !!!
|
||||
// Indicates whether upper case or lower case should sort first.
|
||||
//
|
||||
// This permission is ignored if:
|
||||
// - organizeImportsCollation is not `unicode`
|
||||
// - organizeImportsIgnoreCase is `true`
|
||||
// - organizeImportsIgnoreCase is `auto` and the auto-detected case sensitivity is case-insensitive.
|
||||
//
|
||||
// Default: `false`
|
||||
OrganizeImportsCaseFirst OrganizeImportsCaseFirst `raw:"organizeImportsCaseFirst" config:"preferences.organizeImports.caseFirst"` // !!!
|
||||
// Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is type-only.
|
||||
//
|
||||
// Default: `auto`, which defaults to `last`
|
||||
OrganizeImportsTypeOrder OrganizeImportsTypeOrder `raw:"organizeImportsTypeOrder" config:"preferences.organizeImports.typeOrder"` // !!!
|
||||
|
||||
// ------- MoveToFile -------
|
||||
|
||||
AllowTextChangesInNewFiles core.Tristate `raw:"allowTextChangesInNewFiles"` // !!!
|
||||
|
||||
// ------- Rename -------
|
||||
|
||||
UseAliasesForRename core.Tristate `raw:"providePrefixAndSuffixTextForRename" config:"preferences.useAliasesForRenames"`
|
||||
AllowRenameOfImportPath core.Tristate `raw:"allowRenameOfImportPath"`
|
||||
|
||||
// ------- CodeFixes/Refactors -------
|
||||
|
||||
ProvideRefactorNotApplicableReason core.Tristate `raw:"provideRefactorNotApplicableReason"` // !!!
|
||||
|
||||
// ------- InlayHints -------
|
||||
|
||||
InlayHints InlayHintsPreferences
|
||||
|
||||
// ------- CodeLens -------
|
||||
|
||||
CodeLens CodeLensUserPreferences
|
||||
|
||||
// ------- Definition -------
|
||||
|
||||
PreferGoToSourceDefinition bool `raw:"preferGoToSourceDefinition"`
|
||||
|
||||
// ------- Symbols -------
|
||||
|
||||
ExcludeLibrarySymbolsInNavTo core.Tristate `raw:"excludeLibrarySymbolsInNavTo" config:"workspaceSymbols.excludeLibrarySymbols"`
|
||||
|
||||
// ------- Misc -------
|
||||
|
||||
EnableFormatting core.Tristate `raw:"formatEnabled" config:"format.enabled" fallbackConfig:"format.enable"`
|
||||
EnableValidation core.Tristate `raw:"validateEnabled" config:"validate.enabled" fallbackConfig:"validate.enable"`
|
||||
DisableSuggestions core.Tristate `raw:"disableSuggestions"` // !!!
|
||||
DisableLineTextInReferences core.Tristate `raw:"disableLineTextInReferences"` // !!!
|
||||
DisplayPartsForJSDoc core.Tristate `raw:"displayPartsForJSDoc"` // !!!
|
||||
ReportStyleChecksAsWarnings core.Tristate `raw:"reportStyleChecksAsWarnings" config:"reportStyleChecksAsWarnings"`
|
||||
|
||||
// ------- ATA -------
|
||||
|
||||
// DisableAutomaticTypeAcquisition is the deprecated setting from typescript.disableAutomaticTypeAcquisition.
|
||||
DisableAutomaticTypeAcquisition core.Tristate `raw:"disableAutomaticTypeAcquisition" config:"disableAutomaticTypeAcquisition"`
|
||||
// AutomaticTypeAcquisitionEnabled is the unified setting from tsserver.automaticTypeAcquisition.enabled under the js/ts section.
|
||||
// When set, it takes precedence over DisableAutomaticTypeAcquisition.
|
||||
AutomaticTypeAcquisitionEnabled core.Tristate `raw:"automaticTypeAcquisitionEnabled" config:"tsserver.automaticTypeAcquisition.enabled"`
|
||||
// TODO: add tsserver.web.typeAcquisition.enabled under the js/ts section for the web variant when web support is implemented.
|
||||
|
||||
// ------- Project Configuration -------
|
||||
|
||||
// CustomConfigFileName specifies a custom config file name to use before defaulting to tsconfig.json/jsconfig.json.
|
||||
CustomConfigFileName string `raw:"customConfigFileName" config:"customConfigFileName"`
|
||||
}
|
||||
|
||||
// IsATADisabled returns whether Automatic Type Acquisition is disabled based on user preferences.
|
||||
// It checks the unified setting (tsserver.automaticTypeAcquisition.enabled) first,
|
||||
// then falls back to the deprecated setting (disableAutomaticTypeAcquisition).
|
||||
func (p UserPreferences) IsATADisabled() bool {
|
||||
if !p.AutomaticTypeAcquisitionEnabled.IsUnknown() {
|
||||
return !p.AutomaticTypeAcquisitionEnabled.IsTrue()
|
||||
}
|
||||
return p.DisableAutomaticTypeAcquisition.IsTrue()
|
||||
}
|
||||
|
||||
type InlayHintsPreferences struct {
|
||||
IncludeInlayParameterNameHints IncludeInlayParameterNameHints `raw:"includeInlayParameterNameHints" config:"inlayHints.parameterNames.enabled"`
|
||||
IncludeInlayParameterNameHintsWhenArgumentMatchesName core.Tristate `raw:"includeInlayParameterNameHintsWhenArgumentMatchesName" config:"inlayHints.parameterNames.suppressWhenArgumentMatchesName,invert"`
|
||||
IncludeInlayFunctionParameterTypeHints core.Tristate `raw:"includeInlayFunctionParameterTypeHints" config:"inlayHints.parameterTypes.enabled"`
|
||||
IncludeInlayVariableTypeHints core.Tristate `raw:"includeInlayVariableTypeHints" config:"inlayHints.variableTypes.enabled"`
|
||||
IncludeInlayVariableTypeHintsWhenTypeMatchesName core.Tristate `raw:"includeInlayVariableTypeHintsWhenTypeMatchesName" config:"inlayHints.variableTypes.suppressWhenTypeMatchesName,invert"`
|
||||
IncludeInlayPropertyDeclarationTypeHints core.Tristate `raw:"includeInlayPropertyDeclarationTypeHints" config:"inlayHints.propertyDeclarationTypes.enabled"`
|
||||
IncludeInlayFunctionLikeReturnTypeHints core.Tristate `raw:"includeInlayFunctionLikeReturnTypeHints" config:"inlayHints.functionLikeReturnTypes.enabled"`
|
||||
IncludeInlayEnumMemberValueHints core.Tristate `raw:"includeInlayEnumMemberValueHints" config:"inlayHints.enumMemberValues.enabled"`
|
||||
}
|
||||
|
||||
type CodeLensUserPreferences struct {
|
||||
ReferencesCodeLensEnabled core.Tristate `raw:"referencesCodeLensEnabled" config:"referencesCodeLens.enabled"`
|
||||
ImplementationsCodeLensEnabled core.Tristate `raw:"implementationsCodeLensEnabled" config:"implementationsCodeLens.enabled"`
|
||||
ReferencesCodeLensShowOnAllFunctions core.Tristate `raw:"referencesCodeLensShowOnAllFunctions" config:"referencesCodeLens.showOnAllFunctions"`
|
||||
ImplementationsCodeLensShowOnInterfaceMethods core.Tristate `raw:"implementationsCodeLensShowOnInterfaceMethods" config:"implementationsCodeLens.showOnInterfaceMethods"`
|
||||
ImplementationsCodeLensShowOnAllClassMethods core.Tristate `raw:"implementationsCodeLensShowOnAllClassMethods" config:"implementationsCodeLens.showOnAllClassMethods"`
|
||||
}
|
||||
|
||||
// --- Enum Types ---
|
||||
|
||||
type QuotePreference string
|
||||
|
||||
const (
|
||||
QuotePreferenceUnknown QuotePreference = ""
|
||||
QuotePreferenceAuto QuotePreference = "auto"
|
||||
QuotePreferenceDouble QuotePreference = "double"
|
||||
QuotePreferenceSingle QuotePreference = "single"
|
||||
)
|
||||
|
||||
type JsxAttributeCompletionStyle string
|
||||
|
||||
const (
|
||||
JsxAttributeCompletionStyleUnknown JsxAttributeCompletionStyle = ""
|
||||
JsxAttributeCompletionStyleAuto JsxAttributeCompletionStyle = "auto"
|
||||
JsxAttributeCompletionStyleBraces JsxAttributeCompletionStyle = "braces"
|
||||
JsxAttributeCompletionStyleNone JsxAttributeCompletionStyle = "none"
|
||||
)
|
||||
|
||||
type IncludeInlayParameterNameHints string
|
||||
|
||||
const (
|
||||
IncludeInlayParameterNameHintsNone IncludeInlayParameterNameHints = ""
|
||||
IncludeInlayParameterNameHintsAll IncludeInlayParameterNameHints = "all"
|
||||
IncludeInlayParameterNameHintsLiterals IncludeInlayParameterNameHints = "literals"
|
||||
)
|
||||
|
||||
type OrganizeImportsSort int
|
||||
|
||||
const (
|
||||
OrganizeImportsSortAuto OrganizeImportsSort = iota
|
||||
OrganizeImportsSortOrdinal
|
||||
OrganizeImportsSortOrdinalIgnoreCase
|
||||
OrganizeImportsSortNatural
|
||||
OrganizeImportsSortNaturalIgnoreCase
|
||||
)
|
||||
|
||||
type OrganizeImportsCollation bool
|
||||
|
||||
const (
|
||||
OrganizeImportsCollationOrdinal OrganizeImportsCollation = false
|
||||
OrganizeImportsCollationUnicode OrganizeImportsCollation = true
|
||||
)
|
||||
|
||||
type OrganizeImportsCaseFirst int
|
||||
|
||||
const (
|
||||
OrganizeImportsCaseFirstFalse OrganizeImportsCaseFirst = 0
|
||||
OrganizeImportsCaseFirstLower OrganizeImportsCaseFirst = 1
|
||||
OrganizeImportsCaseFirstUpper OrganizeImportsCaseFirst = 2
|
||||
)
|
||||
|
||||
type OrganizeImportsTypeOrder int
|
||||
|
||||
const (
|
||||
OrganizeImportsTypeOrderAuto OrganizeImportsTypeOrder = 0
|
||||
OrganizeImportsTypeOrderLast OrganizeImportsTypeOrder = 1
|
||||
OrganizeImportsTypeOrderInline OrganizeImportsTypeOrder = 2
|
||||
OrganizeImportsTypeOrderFirst OrganizeImportsTypeOrder = 3
|
||||
)
|
||||
|
||||
// --- Reflection-based parsing infrastructure ---
|
||||
|
||||
// typeParsers maps reflect.Type to a function that parses a value into that type.
|
||||
var typeParsers = map[reflect.Type]func(any) any{
|
||||
reflect.TypeFor[core.Tristate](): func(val any) any {
|
||||
if b, ok := val.(bool); ok {
|
||||
if b {
|
||||
return core.TSTrue
|
||||
}
|
||||
return core.TSFalse
|
||||
}
|
||||
return core.TSUnknown
|
||||
},
|
||||
reflect.TypeFor[IndentStyle](): func(val any) any {
|
||||
return parseIndentStyle(val)
|
||||
},
|
||||
reflect.TypeFor[SemicolonPreference](): func(val any) any {
|
||||
return parseSemicolonPreference(val)
|
||||
},
|
||||
reflect.TypeFor[QuotePreference](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "auto":
|
||||
return QuotePreferenceAuto
|
||||
case "double":
|
||||
return QuotePreferenceDouble
|
||||
case "single":
|
||||
return QuotePreferenceSingle
|
||||
}
|
||||
}
|
||||
return QuotePreferenceUnknown
|
||||
},
|
||||
reflect.TypeFor[JsxAttributeCompletionStyle](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "braces":
|
||||
return JsxAttributeCompletionStyleBraces
|
||||
case "none":
|
||||
return JsxAttributeCompletionStyleNone
|
||||
}
|
||||
}
|
||||
return JsxAttributeCompletionStyleAuto
|
||||
},
|
||||
reflect.TypeFor[IncludeInlayParameterNameHints](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch s {
|
||||
case "all":
|
||||
return IncludeInlayParameterNameHintsAll
|
||||
case "literals":
|
||||
return IncludeInlayParameterNameHintsLiterals
|
||||
}
|
||||
}
|
||||
return IncludeInlayParameterNameHintsNone
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsSort](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "ordinal":
|
||||
return OrganizeImportsSortOrdinal
|
||||
case "ordinalignorecase":
|
||||
return OrganizeImportsSortOrdinalIgnoreCase
|
||||
case "natural":
|
||||
return OrganizeImportsSortNatural
|
||||
case "naturalignorecase":
|
||||
return OrganizeImportsSortNaturalIgnoreCase
|
||||
}
|
||||
}
|
||||
return OrganizeImportsSortAuto
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsCollation](): func(val any) any {
|
||||
if s, ok := val.(string); ok && strings.ToLower(s) == "unicode" {
|
||||
return OrganizeImportsCollationUnicode
|
||||
}
|
||||
return OrganizeImportsCollationOrdinal
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsCaseFirst](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch s {
|
||||
case "lower":
|
||||
return OrganizeImportsCaseFirstLower
|
||||
case "upper":
|
||||
return OrganizeImportsCaseFirstUpper
|
||||
}
|
||||
}
|
||||
return OrganizeImportsCaseFirstFalse
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsTypeOrder](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch s {
|
||||
case "last":
|
||||
return OrganizeImportsTypeOrderLast
|
||||
case "inline":
|
||||
return OrganizeImportsTypeOrderInline
|
||||
case "first":
|
||||
return OrganizeImportsTypeOrderFirst
|
||||
}
|
||||
}
|
||||
return OrganizeImportsTypeOrderAuto
|
||||
},
|
||||
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierPreference](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "project-relative":
|
||||
return modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative
|
||||
case "relative":
|
||||
return modulespecifiers.ImportModuleSpecifierPreferenceRelative
|
||||
case "non-relative":
|
||||
return modulespecifiers.ImportModuleSpecifierPreferenceNonRelative
|
||||
}
|
||||
}
|
||||
return modulespecifiers.ImportModuleSpecifierPreferenceShortest
|
||||
},
|
||||
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierEndingPreference](): func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "minimal":
|
||||
return modulespecifiers.ImportModuleSpecifierEndingPreferenceMinimal
|
||||
case "index":
|
||||
return modulespecifiers.ImportModuleSpecifierEndingPreferenceIndex
|
||||
case "js":
|
||||
return modulespecifiers.ImportModuleSpecifierEndingPreferenceJs
|
||||
}
|
||||
}
|
||||
return modulespecifiers.ImportModuleSpecifierEndingPreferenceAuto
|
||||
},
|
||||
}
|
||||
|
||||
// typeSerializers maps reflect.Type to a function that serializes a value of that type.
|
||||
// For types which do not serialize as-is (tristate, enums, etc).
|
||||
var typeSerializers = map[reflect.Type]func(any) any{
|
||||
reflect.TypeFor[core.Tristate](): func(val any) any {
|
||||
switch val.(core.Tristate) {
|
||||
case core.TSTrue:
|
||||
return true
|
||||
case core.TSFalse:
|
||||
return false
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsSort](): func(val any) any {
|
||||
switch val.(OrganizeImportsSort) {
|
||||
case OrganizeImportsSortOrdinal:
|
||||
return "ordinal"
|
||||
case OrganizeImportsSortOrdinalIgnoreCase:
|
||||
return "ordinalIgnoreCase"
|
||||
case OrganizeImportsSortNatural:
|
||||
return "natural"
|
||||
case OrganizeImportsSortNaturalIgnoreCase:
|
||||
return "naturalIgnoreCase"
|
||||
default:
|
||||
return "auto"
|
||||
}
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsCollation](): func(val any) any {
|
||||
if val.(OrganizeImportsCollation) == OrganizeImportsCollationUnicode {
|
||||
return "unicode"
|
||||
}
|
||||
return "ordinal"
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsCaseFirst](): func(val any) any {
|
||||
switch val.(OrganizeImportsCaseFirst) {
|
||||
case OrganizeImportsCaseFirstLower:
|
||||
return "lower"
|
||||
case OrganizeImportsCaseFirstUpper:
|
||||
return "upper"
|
||||
default:
|
||||
return "default"
|
||||
}
|
||||
},
|
||||
reflect.TypeFor[OrganizeImportsTypeOrder](): func(val any) any {
|
||||
switch val.(OrganizeImportsTypeOrder) {
|
||||
case OrganizeImportsTypeOrderLast:
|
||||
return "last"
|
||||
case OrganizeImportsTypeOrderInline:
|
||||
return "inline"
|
||||
case OrganizeImportsTypeOrderFirst:
|
||||
return "first"
|
||||
default:
|
||||
return "auto"
|
||||
}
|
||||
},
|
||||
// These enums distinguish an unset zero value (e.g. "") from their effective
|
||||
// default (e.g. "auto"): the parser promotes unset/unknown input to the
|
||||
// non-zero default. Plain string serialization would therefore write "" for
|
||||
// an unset field and the parser would read it back as the non-zero default,
|
||||
// breaking round-tripping. Mirror the core.Tristate serializer above and omit
|
||||
// the unset value (return nil) so it decodes back to the zero value. (Enums
|
||||
// whose default already is their zero value, like the OrganizeImports* ones,
|
||||
// round-trip without this.)
|
||||
//
|
||||
// TODO: These three are the only parsers whose fallback is a non-zero value;
|
||||
// every other parser returns its zero value as the fallback. They should be
|
||||
// made consistent: change the parser fallback to return the zero value and
|
||||
// remove this serializer (relying on the default string serialization, which
|
||||
// already omits ""). The consumer must then treat the zero value as the
|
||||
// effective default. The two module-specifier enums are safe to convert (all
|
||||
// read sites already treat the "" zero identically to the promoted default).
|
||||
reflect.TypeFor[JsxAttributeCompletionStyle](): func(val any) any {
|
||||
// TODO: make consistent with other enums (see note above). Unlike the
|
||||
// module-specifier enums, the consumer in completions.go distinguishes
|
||||
// JsxAttributeCompletionStyleUnknown from ...Auto, so converting this one
|
||||
// requires updating that consumer to treat the zero value as "auto".
|
||||
if v := val.(JsxAttributeCompletionStyle); v != JsxAttributeCompletionStyleUnknown {
|
||||
return string(v)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierPreference](): func(val any) any {
|
||||
// TODO: make consistent with other enums (see note above): have the parser
|
||||
// return the zero value (None) as its fallback and drop this serializer.
|
||||
if v := val.(modulespecifiers.ImportModuleSpecifierPreference); v != "" {
|
||||
return string(v)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
reflect.TypeFor[modulespecifiers.ImportModuleSpecifierEndingPreference](): func(val any) any {
|
||||
// TODO: make consistent with other enums (see note above): have the parser
|
||||
// return the zero value (None) as its fallback and drop this serializer.
|
||||
if v := val.(modulespecifiers.ImportModuleSpecifierEndingPreference); v != "" {
|
||||
return string(v)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// configPathParsers provides field-specific config value parsers that override the default
|
||||
// type-based parser when the VS Code config value format differs from the Go field type.
|
||||
var configPathParsers = map[string]func(any) any{
|
||||
// VS Code sends caseSensitivity as a string ("auto"/"caseSensitive"/"caseInsensitive"),
|
||||
// but OrganizeImportsIgnoreCase is a core.Tristate.
|
||||
"preferences.organizeImports.caseSensitivity": func(val any) any {
|
||||
if s, ok := val.(string); ok {
|
||||
switch strings.ToLower(s) {
|
||||
case "caseinsensitive":
|
||||
return core.TSTrue
|
||||
case "casesensitive":
|
||||
return core.TSFalse
|
||||
}
|
||||
}
|
||||
if b, ok := val.(bool); ok {
|
||||
if b {
|
||||
return core.TSTrue
|
||||
}
|
||||
return core.TSFalse
|
||||
}
|
||||
return core.TSUnknown
|
||||
},
|
||||
}
|
||||
|
||||
type fieldInfo struct {
|
||||
rawName string // raw name for unstable section lookup (e.g., "quotePreference")
|
||||
configPath string // dotted path for config (e.g., "preferences.quoteStyle")
|
||||
fallbackConfigPaths []configPathInfo
|
||||
fieldPath []int // index path to field in struct
|
||||
rawInvert bool // whether to invert boolean values for raw name
|
||||
configInvert bool // whether to invert boolean values for config path
|
||||
}
|
||||
|
||||
type configPathInfo struct {
|
||||
path string
|
||||
invert bool
|
||||
}
|
||||
|
||||
var fieldInfoCache = sync.OnceValue(func() []fieldInfo {
|
||||
return collectFieldInfos(reflect.TypeFor[UserPreferences](), nil)
|
||||
})
|
||||
|
||||
// unstableNameIndex maps raw names to fieldInfo index for unstable section lookup.
|
||||
var unstableNameIndex = sync.OnceValue(func() map[string]int {
|
||||
infos := fieldInfoCache()
|
||||
index := make(map[string]int, len(infos))
|
||||
for i, info := range infos {
|
||||
if info.rawName != "" {
|
||||
index[info.rawName] = i
|
||||
}
|
||||
}
|
||||
return index
|
||||
})
|
||||
|
||||
func collectFieldInfos(t reflect.Type, indexPath []int) []fieldInfo {
|
||||
var infos []fieldInfo
|
||||
for i := range t.NumField() {
|
||||
field := t.Field(i)
|
||||
currentPath := append(slices.Clone(indexPath), i)
|
||||
|
||||
rawTag := field.Tag.Get("raw")
|
||||
configTag := field.Tag.Get("config")
|
||||
fallbackConfigTag := field.Tag.Get("fallbackConfig")
|
||||
|
||||
if rawTag == "" && configTag == "" {
|
||||
// Embedded struct without tags - recurse into it
|
||||
if field.Type.Kind() == reflect.Struct {
|
||||
infos = append(infos, collectFieldInfos(field.Type, currentPath)...)
|
||||
continue
|
||||
}
|
||||
panic("raw or config tag required for field " + field.Name)
|
||||
}
|
||||
|
||||
info := fieldInfo{
|
||||
fieldPath: currentPath,
|
||||
}
|
||||
|
||||
// Parse raw tag: "name" or "name,invert"
|
||||
if rawTag != "" {
|
||||
parts := strings.Split(rawTag, ",")
|
||||
info.rawName = parts[0]
|
||||
for _, part := range parts[1:] {
|
||||
if part == "invert" {
|
||||
info.rawInvert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse config tag: "path.to.setting" or "path.to.setting,invert"
|
||||
if configTag != "" {
|
||||
configPath := parseConfigPathTag(configTag)
|
||||
info.configPath = configPath.path
|
||||
info.configInvert = configPath.invert
|
||||
}
|
||||
if fallbackConfigTag != "" {
|
||||
for tag := range strings.SplitSeq(fallbackConfigTag, ";") {
|
||||
info.fallbackConfigPaths = append(info.fallbackConfigPaths, parseConfigPathTag(tag))
|
||||
}
|
||||
}
|
||||
|
||||
infos = append(infos, info)
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
func parseConfigPathTag(tag string) configPathInfo {
|
||||
parts := strings.Split(tag, ",")
|
||||
info := configPathInfo{path: parts[0]}
|
||||
for _, part := range parts[1:] {
|
||||
if part == "invert" {
|
||||
info.invert = true
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func getNestedValue(config map[string]any, path string) (any, bool) {
|
||||
parts := strings.Split(path, ".")
|
||||
current := any(config)
|
||||
for _, part := range parts {
|
||||
m, ok := current.(map[string]any)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
current, ok = m[part]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return current, true
|
||||
}
|
||||
|
||||
func setNestedValue(config map[string]any, path string, value any) {
|
||||
parts := strings.Split(path, ".")
|
||||
current := config
|
||||
for _, part := range parts[:len(parts)-1] {
|
||||
next, ok := current[part].(map[string]any)
|
||||
if !ok {
|
||||
next = make(map[string]any)
|
||||
current[part] = next
|
||||
}
|
||||
current = next
|
||||
}
|
||||
current[parts[len(parts)-1]] = value
|
||||
}
|
||||
|
||||
func setRawFieldsFromConfig(v reflect.Value, infos []fieldInfo, settings map[string]any) {
|
||||
index := unstableNameIndex()
|
||||
for name, value := range settings {
|
||||
if idx, found := index[name]; found {
|
||||
info := infos[idx]
|
||||
field := getFieldByPath(v, info.fieldPath)
|
||||
if info.rawInvert {
|
||||
if b, ok := value.(bool); ok {
|
||||
value = !b
|
||||
}
|
||||
}
|
||||
setFieldFromValue(field, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p UserPreferences) withConfig(config map[string]any) UserPreferences {
|
||||
v := reflect.ValueOf(&p).Elem()
|
||||
infos := fieldInfoCache()
|
||||
|
||||
// Raw UserPreferences can be provided directly, notably via LSP initializationOptions.
|
||||
setRawFieldsFromConfig(v, infos, config)
|
||||
|
||||
// Process "unstable" section first - allows any field to be set by raw name.
|
||||
// This mirrors VS Code's behavior: { ...config.get('unstable'), ...stableOptions }
|
||||
// where stable options are spread after and take precedence.
|
||||
if unstable, ok := config["unstable"].(map[string]any); ok {
|
||||
setRawFieldsFromConfig(v, infos, unstable)
|
||||
}
|
||||
|
||||
// Process path-based config (VS Code style nested paths).
|
||||
// These run after unstable, so stable config values take precedence.
|
||||
for _, info := range infos {
|
||||
if info.configPath == "" {
|
||||
continue
|
||||
}
|
||||
configPath := configPathInfo{path: info.configPath, invert: info.configInvert}
|
||||
val, ok := getNestedValue(config, configPath.path)
|
||||
if !ok {
|
||||
for _, fallbackConfigPath := range info.fallbackConfigPaths {
|
||||
val, ok = getNestedValue(config, fallbackConfigPath.path)
|
||||
if ok {
|
||||
configPath = fallbackConfigPath
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
field := getFieldByPath(v, info.fieldPath)
|
||||
if configPath.invert {
|
||||
if b, ok := val.(bool); ok {
|
||||
val = !b
|
||||
}
|
||||
}
|
||||
if parser, ok := configPathParsers[configPath.path]; ok {
|
||||
field.Set(reflect.ValueOf(parser(val)))
|
||||
continue
|
||||
}
|
||||
setFieldFromValue(field, val)
|
||||
}
|
||||
|
||||
// Validate CustomConfigFileName for path traversal
|
||||
if p.CustomConfigFileName != "" {
|
||||
name := strings.TrimSpace(p.CustomConfigFileName)
|
||||
if strings.ContainsAny(name, "/\\") || name == ".." || name == "." {
|
||||
p.CustomConfigFileName = ""
|
||||
} else {
|
||||
p.CustomConfigFileName = name
|
||||
}
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func getFieldByPath(v reflect.Value, path []int) reflect.Value {
|
||||
for _, idx := range path {
|
||||
v = v.Field(idx)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func setFieldFromValue(field reflect.Value, val any) {
|
||||
if val == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check custom parsers first (for types like Tristate, enums, etc.)
|
||||
if parser, ok := typeParsers[field.Type()]; ok {
|
||||
field.Set(reflect.ValueOf(parser(val)))
|
||||
return
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.Bool:
|
||||
if b, ok := val.(bool); ok {
|
||||
field.SetBool(b)
|
||||
}
|
||||
case reflect.Int:
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
field.SetInt(int64(v))
|
||||
case float64:
|
||||
field.SetInt(int64(v))
|
||||
}
|
||||
case reflect.String:
|
||||
if s, ok := val.(string); ok {
|
||||
field.SetString(s)
|
||||
}
|
||||
case reflect.Slice:
|
||||
if arr, ok := val.([]any); ok {
|
||||
result := reflect.MakeSlice(field.Type(), 0, len(arr))
|
||||
for _, item := range arr {
|
||||
if s, ok := item.(string); ok {
|
||||
result = reflect.Append(result, reflect.ValueOf(s))
|
||||
}
|
||||
}
|
||||
field.Set(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserPreferences) MarshalJSONTo(enc *json.Encoder) error {
|
||||
config := make(map[string]any)
|
||||
v := reflect.ValueOf(p).Elem()
|
||||
|
||||
for _, info := range fieldInfoCache() {
|
||||
field := getFieldByPath(v, info.fieldPath)
|
||||
|
||||
val := serializeField(field)
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Prefer config path if available, otherwise use unstable section
|
||||
if info.configPath != "" {
|
||||
if info.configInvert {
|
||||
if b, ok := val.(bool); ok {
|
||||
val = !b
|
||||
}
|
||||
}
|
||||
setNestedValue(config, info.configPath, val)
|
||||
} else if info.rawName != "" {
|
||||
if info.rawInvert {
|
||||
if b, ok := val.(bool); ok {
|
||||
val = !b
|
||||
}
|
||||
}
|
||||
setNestedValue(config, "unstable."+info.rawName, val)
|
||||
}
|
||||
}
|
||||
|
||||
return json.MarshalEncode(enc, config, json.Deterministic(true))
|
||||
}
|
||||
|
||||
func serializeField(field reflect.Value) any {
|
||||
// Check custom serializers first (for types like Tristate, enums, etc.)
|
||||
if serializer, ok := typeSerializers[field.Type()]; ok {
|
||||
return serializer(field.Interface())
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.Bool:
|
||||
return field.Bool()
|
||||
case reflect.Int:
|
||||
// Zero means "unset" for these preference fields. Omit it so a partial
|
||||
// config does not clobber defaults with zeros when round-tripped through
|
||||
// withConfig.
|
||||
i := field.Int()
|
||||
if i == 0 {
|
||||
return nil
|
||||
}
|
||||
return int(i)
|
||||
case reflect.String:
|
||||
// Zero ("") means "unset"; omit it for the same reason as int above.
|
||||
s := field.String()
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
case reflect.Slice:
|
||||
if field.IsNil() {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, field.Len())
|
||||
for i := range field.Len() {
|
||||
result[i] = field.Index(i).String()
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return field.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserPreferences) UnmarshalJSONFrom(dec *json.Decoder) error {
|
||||
var config map[string]any
|
||||
if err := json.UnmarshalDecode(dec, &config); err != nil {
|
||||
return err
|
||||
}
|
||||
// Start with defaults, then overlay parsed values
|
||||
*p = NewDefaultUserPreferences().withConfig(config)
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Helper methods ---
|
||||
|
||||
func (p UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences {
|
||||
return modulespecifiers.UserPreferences{
|
||||
ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference,
|
||||
ImportModuleSpecifierEnding: p.ImportModuleSpecifierEnding,
|
||||
AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes,
|
||||
}
|
||||
}
|
||||
|
||||
func (p UserPreferences) ParsedAutoImportFileExcludePatterns(useCaseSensitiveFileNames bool) *vfsmatch.SpecMatcher {
|
||||
return vfsmatch.NewSpecMatcher(p.AutoImportFileExcludePatterns, "", vfsmatch.UsageExclude, useCaseSensitiveFileNames)
|
||||
}
|
||||
|
||||
func (p UserPreferences) IsModuleSpecifierExcluded(moduleSpecifier string) bool {
|
||||
return modulespecifiers.IsExcludedByRegex(moduleSpecifier, p.AutoImportSpecifierExcludeRegexes)
|
||||
}
|
||||
|
||||
func ParseUserPreferences(items map[string]any) UserPreferences {
|
||||
prefs := NewDefaultUserPreferences()
|
||||
// Apply editor settings first (tabSize, indentSize, etc.) as raw-name defaults,
|
||||
// then overlay language-specific settings with increasing precedence:
|
||||
// editor < javascript < typescript < js/ts
|
||||
if editorItem, ok := items["editor"]; ok && editorItem != nil {
|
||||
if editorSettings, ok := editorItem.(map[string]any); ok {
|
||||
prefs = prefs.withConfig(map[string]any{"unstable": editorSettings})
|
||||
}
|
||||
}
|
||||
// Apply javascript, then typescript, then js/ts (highest precedence).
|
||||
for _, section := range []string{"javascript", "typescript", "js/ts"} {
|
||||
if item, ok := items[section]; ok && item != nil {
|
||||
if settings, ok := item.(map[string]any); ok {
|
||||
prefs = prefs.withConfig(settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
return prefs
|
||||
}
|
||||
644
tools/tsgo/internal/ls/lsutil/userpreferences_test.go
Normal file
644
tools/tsgo/internal/ls/lsutil/userpreferences_test.go
Normal file
@@ -0,0 +1,644 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/modulespecifiers"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func fillNonZeroValues(v reflect.Value) {
|
||||
t := v.Type()
|
||||
for i := range t.NumField() {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
switch field.Kind() {
|
||||
case reflect.Bool:
|
||||
field.SetBool(true)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
field.SetInt(1)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
field.SetUint(1)
|
||||
case reflect.String:
|
||||
val := getValidStringValue(field.Type())
|
||||
field.SetString(val)
|
||||
case reflect.Slice:
|
||||
if field.Type().Elem().Kind() == reflect.String {
|
||||
field.Set(reflect.ValueOf([]string{"test"}))
|
||||
}
|
||||
case reflect.Struct:
|
||||
fillNonZeroValues(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getValidStringValue(t reflect.Type) string {
|
||||
typeName := t.String()
|
||||
switch typeName {
|
||||
case "lsutil.QuotePreference":
|
||||
return string(QuotePreferenceSingle)
|
||||
case "lsutil.JsxAttributeCompletionStyle":
|
||||
return string(JsxAttributeCompletionStyleBraces)
|
||||
case "lsutil.IncludeInlayParameterNameHints":
|
||||
return string(IncludeInlayParameterNameHintsAll)
|
||||
case "lsutil.SemicolonPreference":
|
||||
return string(SemicolonPreferenceInsert)
|
||||
case "modulespecifiers.ImportModuleSpecifierPreference":
|
||||
return string(modulespecifiers.ImportModuleSpecifierPreferenceRelative)
|
||||
case "modulespecifiers.ImportModuleSpecifierEndingPreference":
|
||||
return string(modulespecifiers.ImportModuleSpecifierEndingPreferenceJs)
|
||||
default:
|
||||
return "test"
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserPreferencesRoundtrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var original UserPreferences
|
||||
fillNonZeroValues(reflect.ValueOf(&original).Elem())
|
||||
|
||||
jsonBytes, err := json.Marshal(&original)
|
||||
assert.NilError(t, err)
|
||||
|
||||
t.Run("UnmarshalJSONFrom", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var parsed UserPreferences
|
||||
err2 := json.Unmarshal(jsonBytes, &parsed)
|
||||
assert.NilError(t, err2)
|
||||
assert.DeepEqual(t, original, parsed)
|
||||
})
|
||||
|
||||
t.Run("withConfig", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var config map[string]any
|
||||
err2 := json.Unmarshal(jsonBytes, &config)
|
||||
assert.NilError(t, err2)
|
||||
parsed := UserPreferences{}.withConfig(config)
|
||||
assert.DeepEqual(t, original, parsed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPreferencesSerialize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("config path field serializes to nested path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := &UserPreferences{
|
||||
QuotePreference: QuotePreferenceSingle,
|
||||
}
|
||||
jsonBytes, err := json.Marshal(prefs)
|
||||
assert.NilError(t, err)
|
||||
|
||||
var actual map[string]any
|
||||
err = json.Unmarshal(jsonBytes, &actual)
|
||||
assert.NilError(t, err)
|
||||
|
||||
preferences := actual["preferences"].(map[string]any)
|
||||
assert.Equal(t, "single", preferences["quoteStyle"])
|
||||
})
|
||||
|
||||
t.Run("raw-only field serializes to unstable section", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := &UserPreferences{
|
||||
DisableSuggestions: core.TSTrue,
|
||||
}
|
||||
jsonBytes, err := json.Marshal(prefs)
|
||||
assert.NilError(t, err)
|
||||
|
||||
var actual map[string]any
|
||||
err = json.Unmarshal(jsonBytes, &actual)
|
||||
assert.NilError(t, err)
|
||||
|
||||
unstable := actual["unstable"].(map[string]any)
|
||||
assert.Equal(t, true, unstable["disableSuggestions"])
|
||||
})
|
||||
|
||||
t.Run("inlay hint inversion on serialize", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := &UserPreferences{
|
||||
InlayHints: InlayHintsPreferences{
|
||||
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
|
||||
IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSTrue,
|
||||
},
|
||||
}
|
||||
jsonBytes, err := json.Marshal(prefs)
|
||||
assert.NilError(t, err)
|
||||
|
||||
var actual map[string]any
|
||||
err = json.Unmarshal(jsonBytes, &actual)
|
||||
assert.NilError(t, err)
|
||||
|
||||
inlayHints := actual["inlayHints"].(map[string]any)
|
||||
parameterNames := inlayHints["parameterNames"].(map[string]any)
|
||||
assert.Equal(t, "all", parameterNames["enabled"])
|
||||
assert.Equal(t, false, parameterNames["suppressWhenArgumentMatchesName"]) // inverted
|
||||
})
|
||||
|
||||
t.Run("mixed config and unstable fields", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := &UserPreferences{
|
||||
QuotePreference: QuotePreferenceSingle,
|
||||
DisableSuggestions: core.TSTrue,
|
||||
DisplayPartsForJSDoc: core.TSTrue,
|
||||
}
|
||||
jsonBytes, err := json.Marshal(prefs)
|
||||
assert.NilError(t, err)
|
||||
|
||||
var actual map[string]any
|
||||
err = json.Unmarshal(jsonBytes, &actual)
|
||||
assert.NilError(t, err)
|
||||
|
||||
preferences := actual["preferences"].(map[string]any)
|
||||
assert.Equal(t, "single", preferences["quoteStyle"])
|
||||
|
||||
unstable := actual["unstable"].(map[string]any)
|
||||
assert.Equal(t, true, unstable["disableSuggestions"])
|
||||
assert.Equal(t, true, unstable["displayPartsForJSDoc"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPreferencesParseUnstable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
expected UserPreferences
|
||||
}{
|
||||
{
|
||||
name: "unstable fields with correct casing",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"disableSuggestions": true,
|
||||
"maximumHoverLength": 100,
|
||||
"allowRenameOfImportPath": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
DisableSuggestions: core.TSTrue,
|
||||
MaximumHoverLength: 100,
|
||||
AllowRenameOfImportPath: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested preferences path",
|
||||
json: `{
|
||||
"preferences": {
|
||||
"quoteStyle": "single",
|
||||
"useAliasesForRenames": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
QuotePreference: QuotePreferenceSingle,
|
||||
UseAliasesForRename: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "suggest section",
|
||||
json: `{
|
||||
"suggest": {
|
||||
"autoImports": false,
|
||||
"includeCompletionsForImportStatements": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
IncludeCompletionsForModuleExports: core.TSFalse,
|
||||
IncludeCompletionsForImportStatements: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inlayHints with invert",
|
||||
json: `{
|
||||
"inlayHints": {
|
||||
"parameterNames": {
|
||||
"enabled": "all",
|
||||
"suppressWhenArgumentMatchesName": true
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
InlayHints: InlayHintsPreferences{
|
||||
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
|
||||
IncludeInlayParameterNameHintsWhenArgumentMatchesName: core.TSFalse, // inverted
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed config",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"displayPartsForJSDoc": true
|
||||
},
|
||||
"preferences": {
|
||||
"importModuleSpecifier": "relative"
|
||||
},
|
||||
"workspaceSymbols": {
|
||||
"excludeLibrarySymbols": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
DisplayPartsForJSDoc: core.TSTrue,
|
||||
ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceRelative,
|
||||
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stable config overrides unstable",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"quotePreference": "double"
|
||||
},
|
||||
"preferences": {
|
||||
"quoteStyle": "single"
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
QuotePreference: QuotePreferenceSingle, // stable wins
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unstable sets value when no stable config",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"includeAutomaticOptionalChainCompletions": false
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
IncludeAutomaticOptionalChainCompletions: core.TSFalse,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "any field can be passed via unstable by its raw name",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"quotePreference": "double",
|
||||
"includeCompletionsForModuleExports": true,
|
||||
"excludeLibrarySymbolsInNavTo": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
QuotePreference: QuotePreferenceDouble,
|
||||
IncludeCompletionsForModuleExports: core.TSTrue,
|
||||
ExcludeLibrarySymbolsInNavTo: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TypeScript raw names work in unstable section",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"includeCompletionsForModuleExports": true,
|
||||
"quotePreference": "single",
|
||||
"providePrefixAndSuffixTextForRename": true,
|
||||
"includeInlayParameterNameHints": "all",
|
||||
"organizeImportsLocale": "en"
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
IncludeCompletionsForModuleExports: core.TSTrue,
|
||||
QuotePreference: QuotePreferenceSingle,
|
||||
UseAliasesForRename: core.TSTrue,
|
||||
OrganizeImportsLocale: "en",
|
||||
InlayHints: InlayHintsPreferences{
|
||||
IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "old raw organize imports unicode preferences load as raw state",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"organizeImportsCollation": "unicode",
|
||||
"organizeImportsCaseFirst": "upper",
|
||||
"organizeImportsIgnoreCase": false,
|
||||
"organizeImportsNumericCollation": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper,
|
||||
OrganizeImportsIgnoreCase: core.TSFalse,
|
||||
OrganizeImportsNumericCollation: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "old top-level raw organize imports unicode preferences load as raw state",
|
||||
json: `{
|
||||
"organizeImportsCollation": "unicode",
|
||||
"organizeImportsIgnoreCase": true
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "new top-level raw organize imports sort is accepted",
|
||||
json: `{
|
||||
"organizeImportsSort": "natural"
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsSort: OrganizeImportsSortNatural,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "old raw organize imports ignore case loads as raw state",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"organizeImportsIgnoreCase": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "new raw organize imports sort loads alongside old raw preferences",
|
||||
json: `{
|
||||
"unstable": {
|
||||
"organizeImportsSort": "ordinal",
|
||||
"organizeImportsCollation": "unicode",
|
||||
"organizeImportsIgnoreCase": true
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsSort: OrganizeImportsSortOrdinal,
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "old nested organize imports unicode preferences load as raw state",
|
||||
json: `{
|
||||
"preferences": {
|
||||
"organizeImports": {
|
||||
"unicodeCollation": "unicode",
|
||||
"caseSensitivity": "caseSensitive",
|
||||
"numericCollation": true,
|
||||
"caseFirst": "upper"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSFalse,
|
||||
OrganizeImportsNumericCollation: core.TSTrue,
|
||||
OrganizeImportsCaseFirst: OrganizeImportsCaseFirstUpper,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "new nested organize imports sort loads alongside old nested preferences",
|
||||
json: `{
|
||||
"preferences": {
|
||||
"organizeImports": {
|
||||
"sort": "ordinalIgnoreCase",
|
||||
"unicodeCollation": "unicode",
|
||||
"caseSensitivity": "caseSensitive"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expected: UserPreferences{
|
||||
OrganizeImportsSort: OrganizeImportsSortOrdinalIgnoreCase,
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSFalse,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var config map[string]any
|
||||
err := json.Unmarshal([]byte(tt.json), &config)
|
||||
assert.NilError(t, err)
|
||||
|
||||
parsed := UserPreferences{}.withConfig(config)
|
||||
|
||||
assert.DeepEqual(t, tt.expected, parsed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserPreferencesReportStyleChecksAsWarnings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("reportStyleChecksAsWarnings via config path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"reportStyleChecksAsWarnings": false,
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("reportStyleChecksAsWarnings defaults to true", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := NewDefaultUserPreferences()
|
||||
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSTrue)
|
||||
})
|
||||
|
||||
t.Run("reportStyleChecksAsWarnings via unstable section", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"unstable": map[string]any{
|
||||
"reportStyleChecksAsWarnings": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.ReportStyleChecksAsWarnings, core.TSFalse)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPreferencesParseServerFeaturePreferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("preferred server feature settings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"validate": map[string]any{"enabled": false},
|
||||
"format": map[string]any{"enabled": false},
|
||||
"autoClosingTags": map[string]any{
|
||||
"enabled": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableValidation, core.TSFalse)
|
||||
assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
|
||||
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("legacy server feature fallbacks", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"validate": map[string]any{"enable": false},
|
||||
"format": map[string]any{"enable": false},
|
||||
"autoClosingTags": false,
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableValidation, core.TSFalse)
|
||||
assert.Equal(t, prefs.EnableFormatting, core.TSFalse)
|
||||
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("preferred settings take precedence over fallbacks", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"validate": map[string]any{"enable": false},
|
||||
"format": map[string]any{"enable": false},
|
||||
"autoClosingTags": false,
|
||||
},
|
||||
"js/ts": map[string]any{
|
||||
"validate": map[string]any{"enabled": true},
|
||||
"format": map[string]any{"enabled": true},
|
||||
"autoClosingTags": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableValidation, core.TSTrue)
|
||||
assert.Equal(t, prefs.EnableFormatting, core.TSTrue)
|
||||
assert.Equal(t, prefs.EnableAutoClosingTags, core.TSTrue)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPreferencesParseJSDocCompletionPreferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("unified jsdoc enabled setting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"jsdoc": map[string]any{
|
||||
"enabled": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("language fallback completeJSDocs setting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"completeJSDocs": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("unified jsdoc enabled takes precedence over language fallback", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"completeJSDocs": false,
|
||||
},
|
||||
},
|
||||
"js/ts": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"jsdoc": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.EnableJSDocCompletions, core.TSTrue)
|
||||
})
|
||||
|
||||
t.Run("unified jsdoc generateReturns setting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"jsdoc": map[string]any{
|
||||
"generateReturns": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("language jsdoc generateReturns setting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"suggest": map[string]any{
|
||||
"jsdoc": map[string]any{
|
||||
"generateReturns": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, prefs.GenerateReturnInDocTemplate, core.TSFalse)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPreferencesParseATA(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("ParseUserPreferences with unified ATA setting in js/ts section", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"js/ts": map[string]any{
|
||||
"tsserver": map[string]any{
|
||||
"automaticTypeAcquisition": map[string]any{
|
||||
"enabled": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Assert(t, prefs.IsATADisabled())
|
||||
assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSFalse)
|
||||
})
|
||||
|
||||
t.Run("ParseUserPreferences with deprecated disableAutomaticTypeAcquisition in typescript section", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"disableAutomaticTypeAcquisition": true,
|
||||
},
|
||||
})
|
||||
assert.Assert(t, prefs.IsATADisabled())
|
||||
assert.Equal(t, prefs.DisableAutomaticTypeAcquisition, core.TSTrue)
|
||||
})
|
||||
|
||||
t.Run("unified setting takes precedence over deprecated setting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Both settings set: unified (js/ts) should take precedence
|
||||
prefs := ParseUserPreferences(map[string]any{
|
||||
"typescript": map[string]any{
|
||||
"disableAutomaticTypeAcquisition": true,
|
||||
},
|
||||
"js/ts": map[string]any{
|
||||
"tsserver": map[string]any{
|
||||
"automaticTypeAcquisition": map[string]any{
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.Assert(t, !prefs.IsATADisabled())
|
||||
assert.Equal(t, prefs.AutomaticTypeAcquisitionEnabled, core.TSTrue)
|
||||
})
|
||||
|
||||
t.Run("IsATADisabled returns false when neither setting is configured", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prefs := NewDefaultUserPreferences()
|
||||
assert.Assert(t, !prefs.IsATADisabled())
|
||||
})
|
||||
}
|
||||
157
tools/tsgo/internal/ls/lsutil/utilities.go
Normal file
157
tools/tsgo/internal/ls/lsutil/utilities.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
func ProbablyUsesSemicolons(file *ast.SourceFile) bool {
|
||||
withSemicolon := 0
|
||||
withoutSemicolon := 0
|
||||
nStatementsToObserve := 5
|
||||
|
||||
var visit func(node *ast.Node) bool
|
||||
visit = func(node *ast.Node) bool {
|
||||
if node.Flags&ast.NodeFlagsReparsed != 0 {
|
||||
return false
|
||||
}
|
||||
if SyntaxRequiresTrailingSemicolonOrASI(node.Kind) {
|
||||
lastToken := GetLastToken(node, file)
|
||||
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
|
||||
withSemicolon++
|
||||
} else {
|
||||
withoutSemicolon++
|
||||
}
|
||||
} else if SyntaxRequiresTrailingCommaOrSemicolonOrASI(node.Kind) {
|
||||
lastToken := GetLastToken(node, file)
|
||||
if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken {
|
||||
withSemicolon++
|
||||
} else if lastToken != nil && lastToken.Kind != ast.KindCommaToken {
|
||||
lastTokenLine := scanner.GetECMALineOfPosition(
|
||||
file,
|
||||
astnav.GetStartOfNode(lastToken, file, false /*includeJSDoc*/),
|
||||
)
|
||||
nextTokenLine := scanner.GetECMALineOfPosition(
|
||||
file,
|
||||
scanner.SkipTrivia(file.Text(), lastToken.End()),
|
||||
)
|
||||
// Avoid counting missing semicolon in single-line objects:
|
||||
// `function f(p: { x: string /*no semicolon here is insignificant*/ }) {`
|
||||
if lastTokenLine != nextTokenLine {
|
||||
withoutSemicolon++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if withSemicolon+withoutSemicolon >= nStatementsToObserve {
|
||||
return true
|
||||
}
|
||||
|
||||
return node.ForEachChild(visit)
|
||||
}
|
||||
|
||||
file.ForEachChild(visit)
|
||||
|
||||
// One statement missing a semicolon isn't sufficient evidence to say the user
|
||||
// doesn't want semicolons, because they may not even be done writing that statement.
|
||||
if withSemicolon == 0 && withoutSemicolon <= 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
// When both kinds of observation exist, treat the file as using semicolons when the
|
||||
// ratio withSemicolon/withoutSemicolon exceeds 1/nStatementsToObserve (real arithmetic),
|
||||
// implemented as an integer inequality to avoid truncation.
|
||||
if withoutSemicolon == 0 {
|
||||
return true
|
||||
}
|
||||
return withSemicolon*nStatementsToObserve > withoutSemicolon
|
||||
}
|
||||
|
||||
func ShouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) core.Tristate {
|
||||
for _, node := range file.Imports() {
|
||||
if core.NodeCoreModules()[node.Text()] && !core.ExclusivelyPrefixedNodeCoreModules[node.Text()] {
|
||||
if strings.HasPrefix(node.Text(), "node:") {
|
||||
return core.TSTrue
|
||||
} else {
|
||||
return core.TSFalse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return program.UsesUriStyleNodeCoreModules()
|
||||
}
|
||||
|
||||
func QuotePreferenceFromString(str *ast.StringLiteral) QuotePreference {
|
||||
if str.TokenFlags&ast.TokenFlagsSingleQuote != 0 {
|
||||
return QuotePreferenceSingle
|
||||
}
|
||||
return QuotePreferenceDouble
|
||||
}
|
||||
|
||||
func GetQuotePreference(sourceFile *ast.SourceFile, preferences UserPreferences) QuotePreference {
|
||||
if preferences.QuotePreference != "" && preferences.QuotePreference != "auto" {
|
||||
if preferences.QuotePreference == "single" {
|
||||
return QuotePreferenceSingle
|
||||
}
|
||||
return QuotePreferenceDouble
|
||||
}
|
||||
// ignore synthetic import added when importHelpers: true
|
||||
firstModuleSpecifier := core.Find(sourceFile.Imports(), func(n *ast.Node) bool {
|
||||
return ast.IsStringLiteral(n) && !ast.NodeIsSynthesized(n.Parent)
|
||||
})
|
||||
if firstModuleSpecifier != nil {
|
||||
return QuotePreferenceFromString(firstModuleSpecifier.AsStringLiteral())
|
||||
}
|
||||
return QuotePreferenceDouble
|
||||
}
|
||||
|
||||
func ModuleSymbolToValidIdentifier(moduleSymbol *ast.Symbol, forceCapitalize bool) string {
|
||||
return ModuleSpecifierToValidIdentifier(stringutil.StripQuotes(moduleSymbol.Name), forceCapitalize)
|
||||
}
|
||||
|
||||
func ModuleSpecifierToValidIdentifier(moduleSpecifier string, forceCapitalize bool) string {
|
||||
baseName := tspath.GetBaseFileName(strings.TrimSuffix(tspath.RemoveFileExtension(moduleSpecifier), "/index"))
|
||||
res := []rune{}
|
||||
lastCharWasValid := true
|
||||
baseNameRunes := []rune(baseName)
|
||||
if len(baseNameRunes) > 0 && scanner.IsIdentifierStart(baseNameRunes[0]) {
|
||||
if forceCapitalize {
|
||||
res = append(res, unicode.ToUpper(baseNameRunes[0]))
|
||||
} else {
|
||||
res = append(res, baseNameRunes[0])
|
||||
}
|
||||
} else {
|
||||
lastCharWasValid = false
|
||||
}
|
||||
|
||||
for i := 1; i < len(baseNameRunes); i++ {
|
||||
isValid := scanner.IsIdentifierPart(baseNameRunes[i])
|
||||
if isValid {
|
||||
if !lastCharWasValid {
|
||||
res = append(res, unicode.ToUpper(baseNameRunes[i]))
|
||||
} else {
|
||||
res = append(res, baseNameRunes[i])
|
||||
}
|
||||
}
|
||||
lastCharWasValid = isValid
|
||||
}
|
||||
|
||||
// Need `"_"` to ensure result isn't empty.
|
||||
resString := string(res)
|
||||
if resString != "" && !IsNonContextualKeyword(scanner.StringToToken(resString)) {
|
||||
return resString
|
||||
}
|
||||
return "_" + resString
|
||||
}
|
||||
|
||||
func IsNonContextualKeyword(token ast.Kind) bool {
|
||||
return ast.IsKeywordKind(token) && !ast.IsContextualKeyword(token)
|
||||
}
|
||||
200
tools/tsgo/internal/ls/lsutil/utilities_test.go
Normal file
200
tools/tsgo/internal/ls/lsutil/utilities_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package lsutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
)
|
||||
|
||||
func parseTS(t *testing.T, text string) *ast.SourceFile {
|
||||
t.Helper()
|
||||
return parser.ParseSourceFile(ast.SourceFileParseOptions{
|
||||
FileName: "/test.ts",
|
||||
Path: "/test.ts",
|
||||
}, text, core.ScriptKindTS)
|
||||
}
|
||||
|
||||
func TestProbablyUsesSemicolons(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "mixed semicolons and ASI favors semicolons when ratio exceeds one fifth",
|
||||
// First five observations: 2 with semicolon, 3 without. Real ratio 2/3 > 1/5.
|
||||
// Integer division bug compared against 1/5==0 and used with/without as ints,
|
||||
// so the old check was effectively (with/without) > 0, which failed here.
|
||||
src: `let a = 1;
|
||||
let b = 2;
|
||||
let c = 3
|
||||
let d = 4
|
||||
let e = 5
|
||||
`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "consistent ASI with no semicolons",
|
||||
src: `let a = 1
|
||||
let b = 2
|
||||
let c = 3
|
||||
`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "consistent semicolons",
|
||||
src: `let a = 1;
|
||||
let b = 2;
|
||||
let c = 3;
|
||||
`,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
file := parseTS(t, tt.src)
|
||||
if got := ProbablyUsesSemicolons(file); got != tt.want {
|
||||
t.Errorf("ProbablyUsesSemicolons() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOrganizeImportsSort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
preferences UserPreferences
|
||||
want OrganizeImportsSort
|
||||
}{
|
||||
{
|
||||
name: "explicit sort wins",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsSort: OrganizeImportsSortOrdinal,
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
want: OrganizeImportsSortOrdinal,
|
||||
},
|
||||
{
|
||||
name: "unicode case-sensitive maps to natural",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSFalse,
|
||||
},
|
||||
want: OrganizeImportsSortNatural,
|
||||
},
|
||||
{
|
||||
name: "unicode ignore case maps to natural ignore case",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
want: OrganizeImportsSortNaturalIgnoreCase,
|
||||
},
|
||||
{
|
||||
name: "unicode unknown case sensitivity stays auto for detection",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsCollation: OrganizeImportsCollationUnicode,
|
||||
},
|
||||
want: OrganizeImportsSortAuto,
|
||||
},
|
||||
{
|
||||
name: "ordinal ignore case maps to ordinal ignore case",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsIgnoreCase: core.TSTrue,
|
||||
},
|
||||
want: OrganizeImportsSortOrdinalIgnoreCase,
|
||||
},
|
||||
{
|
||||
name: "ordinal case sensitive maps to ordinal",
|
||||
preferences: UserPreferences{
|
||||
OrganizeImportsIgnoreCase: core.TSFalse,
|
||||
},
|
||||
want: OrganizeImportsSortOrdinal,
|
||||
},
|
||||
{
|
||||
name: "unknown ordinal stays auto",
|
||||
want: OrganizeImportsSortAuto,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := ResolveOrganizeImportsSort(tt.preferences); got != tt.want {
|
||||
t.Fatalf("ResolveOrganizeImportsSort() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareOrganizeImportsNaturalStrings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
comparer := getOrganizeImportsPresetStringComparer(OrganizeImportsSortNaturalIgnoreCase)
|
||||
tests := []struct {
|
||||
name string
|
||||
a string
|
||||
b string
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "numeric runs sort by numeric value",
|
||||
a: "a2",
|
||||
b: "a100",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "numeric runs with equal value use raw tie break",
|
||||
a: "a02",
|
||||
b: "a2",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "accents are folded for primary comparison",
|
||||
a: "À",
|
||||
b: "B",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "raw comparison breaks accent ties",
|
||||
a: "A",
|
||||
b: "À",
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "hyphen sorts before slash like Intl.Collator fallback",
|
||||
a: "app-init",
|
||||
b: "app/app",
|
||||
want: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := cmpSign(comparer(tt.a, tt.b)); got != tt.want {
|
||||
t.Fatalf("comparer(%q, %q) = %v, want sign %v", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func cmpSign(value int) int {
|
||||
switch {
|
||||
case value < 0:
|
||||
return -1
|
||||
case value > 0:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
954
tools/tsgo/internal/ls/organizeimports.go
Normal file
954
tools/tsgo/internal/ls/organizeimports.go
Normal file
@@ -0,0 +1,954 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/change"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
)
|
||||
|
||||
// OrganizeImports organizes imports by:
|
||||
// 1. Removing unused imports
|
||||
// 2. Coalescing imports from the same module
|
||||
// 3. Sorting imports
|
||||
func (l *LanguageService) OrganizeImports(
|
||||
ctx context.Context,
|
||||
sourceFile *ast.SourceFile,
|
||||
program *compiler.Program,
|
||||
kind lsproto.CodeActionKind,
|
||||
) map[string][]*lsproto.TextEdit {
|
||||
changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters)
|
||||
shouldSort := kind == lsproto.CodeActionKindSourceSortImports || kind == lsproto.CodeActionKindSourceOrganizeImports
|
||||
shouldCombine := shouldSort
|
||||
shouldRemove := kind == lsproto.CodeActionKindSourceRemoveUnusedImports || kind == lsproto.CodeActionKindSourceOrganizeImports
|
||||
topLevelImportDecls := lsutil.FilterImportDeclarations(sourceFile.Statements.Nodes)
|
||||
topLevelImportGroupDecls := groupByNewlineContiguous(sourceFile, topLevelImportDecls)
|
||||
|
||||
preferences := l.UserPreferences()
|
||||
comparersToTest, typeOrdersToTest := lsutil.GetDetectionLists(preferences)
|
||||
defaultComparer := comparersToTest[0]
|
||||
sort := lsutil.ResolveOrganizeImportsSort(preferences)
|
||||
|
||||
var moduleSpecifierComparer func(a, b string) int
|
||||
var namedImportComparer func(a, b string) int
|
||||
if sort != lsutil.OrganizeImportsSortAuto {
|
||||
moduleSpecifierComparer = defaultComparer
|
||||
namedImportComparer = defaultComparer
|
||||
}
|
||||
typeOrder := preferences.OrganizeImportsTypeOrder
|
||||
|
||||
if sort == lsutil.OrganizeImportsSortAuto {
|
||||
result, _ := lsutil.DetectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest)
|
||||
moduleSpecifierComparer = result
|
||||
}
|
||||
|
||||
if typeOrder == lsutil.OrganizeImportsTypeOrderAuto || sort == lsutil.OrganizeImportsSortAuto {
|
||||
namedImportComparer2, typeOrder2, found := lsutil.DetectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest)
|
||||
if found {
|
||||
if namedImportComparer == nil || sort == lsutil.OrganizeImportsSortAuto {
|
||||
namedImportComparer = namedImportComparer2
|
||||
}
|
||||
if typeOrder == lsutil.OrganizeImportsTypeOrderAuto {
|
||||
typeOrder = typeOrder2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
comparer := organizeImportsComparerSettings{
|
||||
moduleSpecifierComparer: moduleSpecifierComparer,
|
||||
namedImportComparer: namedImportComparer,
|
||||
typeOrder: typeOrder,
|
||||
}
|
||||
|
||||
for _, importGroupDecl := range topLevelImportGroupDecls {
|
||||
organizeImportsWorker(importGroupDecl, comparer, shouldSort, shouldCombine, shouldRemove, sourceFile, program, changeTracker, ctx)
|
||||
}
|
||||
|
||||
if kind != lsproto.CodeActionKindSourceRemoveUnusedImports {
|
||||
topLevelExportGroupDecls := getTopLevelExportGroups(sourceFile)
|
||||
for _, exportGroupDecl := range topLevelExportGroupDecls {
|
||||
organizeExportsWorker(exportGroupDecl, comparer, sourceFile, changeTracker)
|
||||
}
|
||||
}
|
||||
|
||||
for _, stmt := range sourceFile.Statements.Nodes {
|
||||
if !ast.IsAmbientModule(stmt.AsNode()) {
|
||||
continue
|
||||
}
|
||||
|
||||
ambientModule := stmt.AsModuleDeclaration()
|
||||
if ambientModule.Body == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
moduleBody := ambientModule.Body.AsModuleBlock()
|
||||
|
||||
ambientModuleImportDecls := lsutil.FilterImportDeclarations(moduleBody.Statements.Nodes)
|
||||
ambientModuleImportGroupDecls := groupByNewlineContiguous(sourceFile, ambientModuleImportDecls)
|
||||
|
||||
for _, importGroupDecl := range ambientModuleImportGroupDecls {
|
||||
organizeImportsWorker(importGroupDecl, comparer, shouldSort, shouldCombine, shouldRemove, sourceFile, program, changeTracker, ctx)
|
||||
}
|
||||
|
||||
if kind != lsproto.CodeActionKindSourceRemoveUnusedImports {
|
||||
var ambientModuleExportDecls []*ast.Statement
|
||||
for _, s := range moduleBody.Statements.Nodes {
|
||||
if s.Kind == ast.KindExportDeclaration {
|
||||
ambientModuleExportDecls = append(ambientModuleExportDecls, s)
|
||||
}
|
||||
}
|
||||
organizeExportsWorker(ambientModuleExportDecls, comparer, sourceFile, changeTracker)
|
||||
}
|
||||
}
|
||||
|
||||
return changeTracker.GetChanges()
|
||||
}
|
||||
|
||||
type organizeImportsComparerSettings struct {
|
||||
moduleSpecifierComparer func(a, b string) int
|
||||
namedImportComparer func(a, b string) int
|
||||
typeOrder lsutil.OrganizeImportsTypeOrder
|
||||
}
|
||||
|
||||
func organizeImportsWorker(
|
||||
oldImportDecls []*ast.Statement,
|
||||
comparer organizeImportsComparerSettings,
|
||||
shouldSort bool,
|
||||
shouldCombine bool,
|
||||
shouldRemove bool,
|
||||
sourceFile *ast.SourceFile,
|
||||
program *compiler.Program,
|
||||
changeTracker *change.Tracker,
|
||||
ctx context.Context,
|
||||
) {
|
||||
if len(oldImportDecls) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Header comment preservation is handled via LeadingTriviaOptionExclude in the change tracker below
|
||||
|
||||
processedImports := slices.Clone(oldImportDecls)
|
||||
if shouldRemove {
|
||||
typeChecker, done := program.GetTypeCheckerForFile(ctx, sourceFile)
|
||||
defer done()
|
||||
processedImports = removeUnusedImports(processedImports, sourceFile, typeChecker, program, changeTracker)
|
||||
}
|
||||
|
||||
var newImportDecls []*ast.Statement
|
||||
if shouldCombine {
|
||||
grouped := groupByModuleSpecifier(processedImports)
|
||||
if shouldSort {
|
||||
slices.SortFunc(grouped, func(a, b []*ast.Statement) int {
|
||||
if len(a) == 0 || len(b) == 0 {
|
||||
return 0
|
||||
}
|
||||
return lsutil.CompareModuleSpecifiers(
|
||||
a[0].ModuleSpecifier(),
|
||||
b[0].ModuleSpecifier(),
|
||||
comparer.moduleSpecifierComparer,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
specifierComparer := lsutil.GetNamedImportSpecifierComparer(
|
||||
lsutil.UserPreferences{OrganizeImportsTypeOrder: comparer.typeOrder},
|
||||
comparer.namedImportComparer,
|
||||
)
|
||||
|
||||
for _, importGroup := range grouped {
|
||||
coalesced := coalesceImportsWorker(importGroup, comparer.moduleSpecifierComparer, specifierComparer, sourceFile, changeTracker)
|
||||
if shouldSort {
|
||||
slices.SortFunc(coalesced, func(a, b *ast.Statement) int {
|
||||
return lsutil.CompareImportsOrRequireStatements(a, b, comparer.moduleSpecifierComparer)
|
||||
})
|
||||
}
|
||||
newImportDecls = append(newImportDecls, coalesced...)
|
||||
}
|
||||
} else {
|
||||
newImportDecls = processedImports
|
||||
}
|
||||
|
||||
if shouldSort && !shouldCombine {
|
||||
slices.SortFunc(newImportDecls, func(a, b *ast.Statement) int {
|
||||
return lsutil.CompareImportsOrRequireStatements(a, b, comparer.moduleSpecifierComparer)
|
||||
})
|
||||
}
|
||||
|
||||
if len(newImportDecls) == 0 {
|
||||
changeTracker.DeleteNodeRange(
|
||||
sourceFile,
|
||||
oldImportDecls[0].AsNode(),
|
||||
oldImportDecls[len(oldImportDecls)-1].AsNode(),
|
||||
change.LeadingTriviaOptionExclude, // Preserve header comment
|
||||
change.TrailingTriviaOptionInclude,
|
||||
)
|
||||
} else {
|
||||
for _, imp := range newImportDecls {
|
||||
changeTracker.SetEmitFlags(imp.AsNode(), printer.EFNoLeadingComments)
|
||||
}
|
||||
|
||||
options := change.NodeOptions{
|
||||
LeadingTriviaOption: change.LeadingTriviaOptionExclude, // Preserve header comment
|
||||
TrailingTriviaOption: change.TrailingTriviaOptionInclude,
|
||||
Suffix: "\n",
|
||||
}
|
||||
|
||||
newNodes := core.Map(newImportDecls, func(s *ast.Statement) *ast.Node { return s.AsNode() })
|
||||
changeTracker.ReplaceNodeWithNodes(sourceFile, oldImportDecls[0].AsNode(), newNodes, &options)
|
||||
|
||||
if len(oldImportDecls) > 1 {
|
||||
for i := 1; i < len(oldImportDecls); i++ {
|
||||
changeTracker.Delete(sourceFile, oldImportDecls[i].AsNode())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func groupByModuleSpecifier(imports []*ast.Statement) [][]*ast.Statement {
|
||||
groups := make(map[string][]*ast.Statement)
|
||||
var order []string
|
||||
|
||||
for _, imp := range imports {
|
||||
specifier := lsutil.GetExternalModuleName(imp.ModuleSpecifier())
|
||||
if _, exists := groups[specifier]; !exists {
|
||||
order = append(order, specifier)
|
||||
}
|
||||
groups[specifier] = append(groups[specifier], imp)
|
||||
}
|
||||
|
||||
result := make([][]*ast.Statement, 0, len(order))
|
||||
for _, key := range order {
|
||||
result = append(result, groups[key])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func removeUnusedImports(oldImports []*ast.Statement, sourceFile *ast.SourceFile, typeChecker *checker.Checker, program *compiler.Program, changeTracker *change.Tracker) []*ast.Statement {
|
||||
compilerOptions := program.Options()
|
||||
jsxElementsPresent := (sourceFile.AsNode().SubtreeFacts() & ast.SubtreeContainsJsx) != 0
|
||||
jsxModeNeedsExplicitImport := compilerOptions.Jsx == core.JsxEmitReact || compilerOptions.Jsx == core.JsxEmitReactNative
|
||||
|
||||
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
|
||||
usedImports := make([]*ast.Statement, 0, len(oldImports))
|
||||
|
||||
for _, importDecl := range oldImports {
|
||||
importClause := importDecl.AsImportDeclaration().ImportClause
|
||||
if importClause == nil {
|
||||
usedImports = append(usedImports, importDecl)
|
||||
continue
|
||||
}
|
||||
|
||||
clause := importClause.AsImportClause()
|
||||
name := clause.Name()
|
||||
namedBindings := clause.NamedBindings
|
||||
|
||||
if name != nil && !typeChecker.IsDeclarationUsed(sourceFile, name.AsIdentifier(), jsxElementsPresent, jsxModeNeedsExplicitImport) {
|
||||
name = nil
|
||||
}
|
||||
|
||||
if namedBindings != nil {
|
||||
switch namedBindings.Kind {
|
||||
case ast.KindNamespaceImport:
|
||||
nsImport := namedBindings.AsNamespaceImport()
|
||||
if !typeChecker.IsDeclarationUsed(sourceFile, nsImport.Name().AsIdentifier(), jsxElementsPresent, jsxModeNeedsExplicitImport) {
|
||||
namedBindings = nil
|
||||
}
|
||||
case ast.KindNamedImports:
|
||||
namedImports := namedBindings.AsNamedImports()
|
||||
originalBindings := namedBindings
|
||||
newElements := filterUsedImportSpecifiers(namedImports.Elements.Nodes, typeChecker, sourceFile, jsxElementsPresent, jsxModeNeedsExplicitImport)
|
||||
if len(newElements) == 0 {
|
||||
namedBindings = nil
|
||||
} else if len(newElements) < len(namedImports.Elements.Nodes) {
|
||||
newList := factory.NewNodeList(newElements)
|
||||
updatedNamedImports := factory.UpdateNamedImports(namedImports, newList)
|
||||
namedBindings = updatedNamedImports.AsNode()
|
||||
}
|
||||
if namedBindings != nil && !ast.NodeIsSynthesized(originalBindings.AsNode()) && !printer.RangeIsOnSingleLine(originalBindings.Loc, sourceFile) {
|
||||
changeTracker.SetEmitFlags(namedBindings, printer.EFMultiLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if name != nil || namedBindings != nil {
|
||||
importDeclNode := importDecl.AsImportDeclaration()
|
||||
newClause := factory.UpdateImportClause(clause, clause.PhaseModifier, name, namedBindings)
|
||||
newImportDecl := factory.UpdateImportDeclaration(
|
||||
importDeclNode,
|
||||
importDeclNode.Modifiers(),
|
||||
newClause.AsNode(),
|
||||
importDeclNode.ModuleSpecifier,
|
||||
importDeclNode.Attributes,
|
||||
)
|
||||
usedImports = append(usedImports, newImportDecl)
|
||||
} else {
|
||||
moduleSpecifier := importDecl.ModuleSpecifier()
|
||||
if hasModuleDeclarationMatchingSpecifier(sourceFile, moduleSpecifier) {
|
||||
if sourceFile.IsDeclarationFile {
|
||||
importDeclNode := importDecl.AsImportDeclaration()
|
||||
newImportDecl := factory.UpdateImportDeclaration(
|
||||
importDeclNode,
|
||||
importDeclNode.Modifiers(),
|
||||
nil, // no import clause
|
||||
importDeclNode.ModuleSpecifier,
|
||||
importDeclNode.Attributes,
|
||||
)
|
||||
usedImports = append(usedImports, newImportDecl)
|
||||
} else {
|
||||
usedImports = append(usedImports, importDecl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return usedImports
|
||||
}
|
||||
|
||||
func filterUsedImportSpecifiers(
|
||||
elements []*ast.Statement,
|
||||
typeChecker *checker.Checker,
|
||||
sourceFile *ast.SourceFile,
|
||||
jsxElementsPresent bool,
|
||||
jsxModeNeedsExplicitImport bool,
|
||||
) []*ast.Statement {
|
||||
var result []*ast.Statement
|
||||
for _, elem := range elements {
|
||||
spec := elem.AsImportSpecifier()
|
||||
if typeChecker.IsDeclarationUsed(sourceFile, spec.Name().AsIdentifier(), jsxElementsPresent, jsxModeNeedsExplicitImport) {
|
||||
result = append(result, elem)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func hasModuleDeclarationMatchingSpecifier(sourceFile *ast.SourceFile, moduleSpecifier *ast.Expression) bool {
|
||||
if moduleSpecifier == nil || !ast.IsStringLiteral(moduleSpecifier.AsNode()) {
|
||||
return false
|
||||
}
|
||||
moduleSpecifierText := moduleSpecifier.Text()
|
||||
|
||||
for _, moduleName := range sourceFile.ModuleAugmentations {
|
||||
if ast.IsStringLiteral(moduleName) && moduleName.Text() == moduleSpecifierText {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getImportAttributesKey returns a key for grouping imports by their attributes.
|
||||
func getImportAttributesKey(attributes *ast.ImportAttributesNode) string {
|
||||
if attributes == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
importAttrs := attributes.AsImportAttributes()
|
||||
var key strings.Builder
|
||||
key.WriteString(importAttrs.Token.String())
|
||||
key.WriteString(" ")
|
||||
|
||||
attrNodes := make([]*ast.Node, len(importAttrs.Attributes.Nodes))
|
||||
copy(attrNodes, importAttrs.Attributes.Nodes)
|
||||
slices.SortFunc(attrNodes, func(a, b *ast.Node) int {
|
||||
aName := a.AsImportAttribute().Name().Text()
|
||||
bName := b.AsImportAttribute().Name().Text()
|
||||
return stringutil.CompareStringsCaseSensitive(aName, bName)
|
||||
})
|
||||
|
||||
for _, attrNode := range attrNodes {
|
||||
attr := attrNode.AsImportAttribute()
|
||||
key.WriteString(attr.Name().Text())
|
||||
key.WriteString(":")
|
||||
if ast.IsStringLiteralLike(attr.Value.AsNode()) {
|
||||
key.WriteString(`"`)
|
||||
key.WriteString(attr.Value.Text())
|
||||
key.WriteString(`"`)
|
||||
} else {
|
||||
key.WriteString(attr.Value.AsNode().Text())
|
||||
}
|
||||
key.WriteString(" ")
|
||||
}
|
||||
|
||||
return key.String()
|
||||
}
|
||||
|
||||
// groupByNewlineContiguous groups declarations by blank lines between them.
|
||||
func groupByNewlineContiguous(sourceFile *ast.SourceFile, decls []*ast.Statement) [][]*ast.Statement {
|
||||
s := scanner.NewScanner()
|
||||
s.SetSkipTrivia(false) // Must not skip trivia to detect newlines
|
||||
var groups [][]*ast.Statement
|
||||
var currentGroup []*ast.Statement
|
||||
|
||||
for _, decl := range decls {
|
||||
if len(currentGroup) > 0 && isNewGroup(sourceFile, decl, s) {
|
||||
groups = append(groups, currentGroup)
|
||||
currentGroup = nil
|
||||
}
|
||||
currentGroup = append(currentGroup, decl)
|
||||
}
|
||||
|
||||
if len(currentGroup) > 0 {
|
||||
groups = append(groups, currentGroup)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
func isNewGroup(sourceFile *ast.SourceFile, decl *ast.Statement, s *scanner.Scanner) bool {
|
||||
fullStart := decl.Pos()
|
||||
if fullStart < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
text := sourceFile.Text()
|
||||
textLen := len(text)
|
||||
|
||||
if fullStart >= textLen {
|
||||
return false
|
||||
}
|
||||
|
||||
startPos := scanner.SkipTrivia(text, fullStart)
|
||||
if startPos <= fullStart {
|
||||
return false
|
||||
}
|
||||
|
||||
triviaLen := startPos - fullStart
|
||||
s.SetText(text[fullStart:startPos])
|
||||
|
||||
numberOfNewLines := 0
|
||||
for s.TokenStart() < triviaLen {
|
||||
tokenKind := s.Scan()
|
||||
if tokenKind == ast.KindNewLineTrivia {
|
||||
numberOfNewLines++
|
||||
if numberOfNewLines >= 2 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func coalesceImportsWorker(
|
||||
importDecls []*ast.Statement,
|
||||
comparer func(a, b string) int,
|
||||
specifierComparer func(s1, s2 *ast.Node) int,
|
||||
sourceFile *ast.SourceFile,
|
||||
changeTracker *change.Tracker,
|
||||
) []*ast.Statement {
|
||||
if len(importDecls) == 0 {
|
||||
return importDecls
|
||||
}
|
||||
|
||||
importGroupsByAttributes := make(map[string][]*ast.Statement)
|
||||
var attributeKeys []string
|
||||
|
||||
for _, importDecl := range importDecls {
|
||||
key := getImportAttributesKey(importDecl.AsImportDeclaration().Attributes)
|
||||
if _, exists := importGroupsByAttributes[key]; !exists {
|
||||
attributeKeys = append(attributeKeys, key)
|
||||
}
|
||||
importGroupsByAttributes[key] = append(importGroupsByAttributes[key], importDecl)
|
||||
}
|
||||
|
||||
coalescedImports := make([]*ast.Statement, 0)
|
||||
|
||||
for _, attributeKey := range attributeKeys {
|
||||
importGroupSameAttrs := importGroupsByAttributes[attributeKey]
|
||||
categorized := getCategorizedImports(importGroupSameAttrs)
|
||||
|
||||
if categorized.importWithoutClause != nil {
|
||||
coalescedImports = append(coalescedImports, categorized.importWithoutClause)
|
||||
}
|
||||
|
||||
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
|
||||
|
||||
for i, group := range []importGroup{categorized.regularImports, categorized.typeOnlyImports} {
|
||||
if group.isEmpty() {
|
||||
continue
|
||||
}
|
||||
|
||||
isTypeOnly := i == 1
|
||||
|
||||
if !isTypeOnly && len(group.defaultImports) == 1 && len(group.namespaceImports) == 1 && len(group.namedImports) == 0 {
|
||||
defaultImport := group.defaultImports[0]
|
||||
namespaceImport := group.namespaceImports[0]
|
||||
|
||||
defaultClause := defaultImport.AsImportDeclaration().ImportClause.AsImportClause()
|
||||
namespaceBindings := namespaceImport.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings
|
||||
|
||||
newClause := factory.UpdateImportClause(defaultClause, defaultClause.PhaseModifier, defaultClause.Name(), namespaceBindings)
|
||||
defaultDeclNode := defaultImport.AsImportDeclaration()
|
||||
newImportDecl := factory.UpdateImportDeclaration(
|
||||
defaultDeclNode,
|
||||
defaultDeclNode.Modifiers(),
|
||||
newClause,
|
||||
defaultDeclNode.ModuleSpecifier,
|
||||
defaultDeclNode.Attributes,
|
||||
)
|
||||
coalescedImports = append(coalescedImports, newImportDecl)
|
||||
continue
|
||||
}
|
||||
|
||||
slices.SortFunc(group.namespaceImports, func(a, b *ast.Statement) int {
|
||||
n1 := a.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings.AsNamespaceImport().Name()
|
||||
n2 := b.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings.AsNamespaceImport().Name()
|
||||
return comparer(n1.Text(), n2.Text())
|
||||
})
|
||||
|
||||
for _, nsImport := range group.namespaceImports {
|
||||
nsImportDecl := nsImport.AsImportDeclaration()
|
||||
clause := nsImportDecl.ImportClause.AsImportClause()
|
||||
newClause := factory.UpdateImportClause(clause, clause.PhaseModifier, nil, clause.NamedBindings)
|
||||
newImportDecl := factory.UpdateImportDeclaration(
|
||||
nsImportDecl,
|
||||
nsImportDecl.Modifiers(),
|
||||
newClause,
|
||||
nsImportDecl.ModuleSpecifier,
|
||||
nsImportDecl.Attributes,
|
||||
)
|
||||
coalescedImports = append(coalescedImports, newImportDecl)
|
||||
}
|
||||
|
||||
var firstDefaultImport *ast.Statement
|
||||
var firstNamedImport *ast.Statement
|
||||
|
||||
if len(group.defaultImports) > 0 {
|
||||
firstDefaultImport = group.defaultImports[0]
|
||||
}
|
||||
if len(group.namedImports) > 0 {
|
||||
firstNamedImport = group.namedImports[0]
|
||||
}
|
||||
|
||||
importDecl := firstDefaultImport
|
||||
if importDecl == nil {
|
||||
importDecl = firstNamedImport
|
||||
}
|
||||
if importDecl == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var newDefaultImport *ast.IdentifierNode
|
||||
var newImportSpecifiers []*ast.Node
|
||||
|
||||
if len(group.defaultImports) == 1 {
|
||||
newDefaultImport = group.defaultImports[0].AsImportDeclaration().ImportClause.AsImportClause().Name()
|
||||
} else {
|
||||
for _, defaultImport := range group.defaultImports {
|
||||
defaultClause := defaultImport.AsImportDeclaration().ImportClause.AsImportClause()
|
||||
defaultName := defaultClause.Name()
|
||||
propertyName := factory.NewIdentifier("default")
|
||||
importSpec := factory.NewImportSpecifier(false, propertyName, defaultName)
|
||||
newImportSpecifiers = append(newImportSpecifiers, importSpec)
|
||||
}
|
||||
}
|
||||
|
||||
newImportSpecifiers = append(newImportSpecifiers, getNewImportSpecifiers(group.namedImports, factory)...)
|
||||
slices.SortStableFunc(newImportSpecifiers, specifierComparer)
|
||||
|
||||
var newNamedImports *ast.NamedImportBindings
|
||||
if len(newImportSpecifiers) == 0 {
|
||||
if newDefaultImport != nil {
|
||||
newNamedImports = nil
|
||||
} else {
|
||||
newNamedImports = factory.NewNamedImports(factory.NewNodeList(nil))
|
||||
}
|
||||
} else {
|
||||
sortedList := factory.NewNodeList(newImportSpecifiers)
|
||||
if firstNamedImport != nil {
|
||||
firstNamedBindings := firstNamedImport.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings.AsNamedImports()
|
||||
originalElements := firstNamedBindings.Elements
|
||||
if originalElements.HasTrailingComma() {
|
||||
sortedList.Loc = originalElements.Loc
|
||||
}
|
||||
newNamedImports = factory.UpdateNamedImports(firstNamedBindings, sortedList).AsNode()
|
||||
} else {
|
||||
newNamedImports = factory.NewNamedImports(sortedList)
|
||||
}
|
||||
}
|
||||
|
||||
if sourceFile != nil && newNamedImports != nil && firstNamedImport != nil {
|
||||
firstNamedBindings := firstNamedImport.AsImportDeclaration().ImportClause.AsImportClause().NamedBindings
|
||||
if !ast.NodeIsSynthesized(firstNamedBindings.AsNode()) && !printer.RangeIsOnSingleLine(firstNamedBindings.Loc, sourceFile) {
|
||||
changeTracker.SetEmitFlags(newNamedImports.AsNode(), printer.EFMultiLine)
|
||||
}
|
||||
}
|
||||
|
||||
if isTypeOnly && newDefaultImport != nil && newNamedImports != nil {
|
||||
importDeclNode := importDecl.AsImportDeclaration()
|
||||
|
||||
defaultClause := factory.NewImportClause(importDeclNode.ImportClause.AsImportClause().PhaseModifier, newDefaultImport, nil)
|
||||
defaultImportDecl := factory.UpdateImportDeclaration(
|
||||
importDeclNode,
|
||||
importDeclNode.Modifiers(),
|
||||
defaultClause,
|
||||
importDeclNode.ModuleSpecifier,
|
||||
importDeclNode.Attributes,
|
||||
)
|
||||
coalescedImports = append(coalescedImports, defaultImportDecl)
|
||||
|
||||
namedDeclNode := firstNamedImport
|
||||
if namedDeclNode == nil {
|
||||
namedDeclNode = importDecl
|
||||
}
|
||||
namedImportDeclNode := namedDeclNode.AsImportDeclaration()
|
||||
namedClause := factory.NewImportClause(namedImportDeclNode.ImportClause.AsImportClause().PhaseModifier, nil, newNamedImports)
|
||||
namedImportDecl := factory.UpdateImportDeclaration(
|
||||
namedImportDeclNode,
|
||||
namedImportDeclNode.Modifiers(),
|
||||
namedClause,
|
||||
namedImportDeclNode.ModuleSpecifier,
|
||||
namedImportDeclNode.Attributes,
|
||||
)
|
||||
coalescedImports = append(coalescedImports, namedImportDecl)
|
||||
} else {
|
||||
importDeclNode := importDecl.AsImportDeclaration()
|
||||
clauseNode := importDeclNode.ImportClause.AsImportClause()
|
||||
newClause := factory.UpdateImportClause(clauseNode, clauseNode.PhaseModifier, newDefaultImport, newNamedImports)
|
||||
newImportDecl := factory.UpdateImportDeclaration(
|
||||
importDeclNode,
|
||||
importDeclNode.Modifiers(),
|
||||
newClause,
|
||||
importDeclNode.ModuleSpecifier,
|
||||
importDeclNode.Attributes,
|
||||
)
|
||||
coalescedImports = append(coalescedImports, newImportDecl)
|
||||
}
|
||||
}
|
||||
}
|
||||
return coalescedImports
|
||||
}
|
||||
|
||||
type categorizedImports struct {
|
||||
importWithoutClause *ast.Statement
|
||||
typeOnlyImports importGroup
|
||||
regularImports importGroup
|
||||
}
|
||||
|
||||
type importGroup struct {
|
||||
defaultImports []*ast.Statement
|
||||
namespaceImports []*ast.Statement
|
||||
namedImports []*ast.Statement
|
||||
}
|
||||
|
||||
func (g importGroup) isEmpty() bool {
|
||||
return len(g.defaultImports) == 0 && len(g.namespaceImports) == 0 && len(g.namedImports) == 0
|
||||
}
|
||||
|
||||
func getCategorizedImports(importDecls []*ast.Statement) categorizedImports {
|
||||
var importWithoutClause *ast.Statement
|
||||
var typeOnlyImports, regularImports importGroup
|
||||
|
||||
for _, importDecl := range importDecls {
|
||||
if importDecl.AsImportDeclaration().ImportClause == nil {
|
||||
if importWithoutClause == nil {
|
||||
importWithoutClause = importDecl
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
clause := importDecl.AsImportDeclaration().ImportClause.AsImportClause()
|
||||
group := ®ularImports
|
||||
if clause.IsTypeOnly() {
|
||||
group = &typeOnlyImports
|
||||
}
|
||||
|
||||
name := clause.Name()
|
||||
namedBindings := clause.NamedBindings
|
||||
|
||||
if name != nil {
|
||||
group.defaultImports = append(group.defaultImports, importDecl)
|
||||
}
|
||||
|
||||
if namedBindings != nil {
|
||||
switch namedBindings.Kind {
|
||||
case ast.KindNamespaceImport:
|
||||
group.namespaceImports = append(group.namespaceImports, importDecl)
|
||||
case ast.KindNamedImports:
|
||||
group.namedImports = append(group.namedImports, importDecl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return categorizedImports{
|
||||
importWithoutClause: importWithoutClause,
|
||||
typeOnlyImports: typeOnlyImports,
|
||||
regularImports: regularImports,
|
||||
}
|
||||
}
|
||||
|
||||
func getNewImportSpecifiers(namedImports []*ast.Statement, factory *ast.NodeFactory) []*ast.Node {
|
||||
var result []*ast.Node
|
||||
|
||||
for _, namedImport := range namedImports {
|
||||
elements := tryGetNamedBindingElements(namedImport)
|
||||
if elements == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, elem := range elements {
|
||||
spec := elem.AsImportSpecifier()
|
||||
|
||||
if spec.PropertyName != nil && spec.Name() != nil {
|
||||
propertyText := spec.PropertyName.Text()
|
||||
nameText := spec.Name().Text()
|
||||
|
||||
if propertyText == nameText {
|
||||
normalized := factory.UpdateImportSpecifier(spec, spec.IsTypeOnly, nil, spec.Name())
|
||||
result = append(result, normalized)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, elem)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func tryGetNamedBindingElements(namedImport *ast.Statement) []*ast.Statement {
|
||||
if namedImport.Kind != ast.KindImportDeclaration {
|
||||
return nil
|
||||
}
|
||||
|
||||
importDecl := namedImport.AsImportDeclaration()
|
||||
if importDecl.ImportClause == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
clause := importDecl.ImportClause.AsImportClause()
|
||||
namedBindings := clause.NamedBindings
|
||||
|
||||
if namedBindings != nil && namedBindings.Kind == ast.KindNamedImports {
|
||||
namedImportsNode := namedBindings.AsNamedImports()
|
||||
return namedImportsNode.Elements.Nodes
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTopLevelExportGroups(sourceFile *ast.SourceFile) [][]*ast.Statement {
|
||||
var topLevelExportGroups [][]*ast.Statement
|
||||
statements := sourceFile.Statements.Nodes
|
||||
statementsLen := len(statements)
|
||||
|
||||
i := 0
|
||||
groupIndex := 0
|
||||
for i < statementsLen {
|
||||
if statements[i].Kind == ast.KindExportDeclaration {
|
||||
if groupIndex >= len(topLevelExportGroups) {
|
||||
topLevelExportGroups = append(topLevelExportGroups, []*ast.Statement{})
|
||||
}
|
||||
exportDecl := statements[i].AsExportDeclaration()
|
||||
if exportDecl.ModuleSpecifier != nil {
|
||||
topLevelExportGroups[groupIndex] = append(topLevelExportGroups[groupIndex], statements[i])
|
||||
i++
|
||||
} else {
|
||||
for i < statementsLen && statements[i].Kind == ast.KindExportDeclaration {
|
||||
topLevelExportGroups[groupIndex] = append(topLevelExportGroups[groupIndex], statements[i])
|
||||
i++
|
||||
}
|
||||
groupIndex++
|
||||
}
|
||||
} else {
|
||||
i++
|
||||
if groupIndex < len(topLevelExportGroups) && len(topLevelExportGroups[groupIndex]) > 0 {
|
||||
groupIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result [][]*ast.Statement
|
||||
for _, exportGroup := range topLevelExportGroups {
|
||||
subGroups := groupByNewlineContiguous(sourceFile, exportGroup)
|
||||
result = append(result, subGroups...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func organizeExportsWorker(
|
||||
oldExportDecls []*ast.Statement,
|
||||
comparer organizeImportsComparerSettings,
|
||||
sourceFile *ast.SourceFile,
|
||||
changeTracker *change.Tracker,
|
||||
) {
|
||||
if len(oldExportDecls) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
specifierComparerFunc := lsutil.GetNamedImportSpecifierComparer(
|
||||
lsutil.UserPreferences{OrganizeImportsTypeOrder: comparer.typeOrder},
|
||||
comparer.namedImportComparer,
|
||||
)
|
||||
|
||||
newExportDecls := coalesceExportsWorker(oldExportDecls, specifierComparerFunc, comparer.moduleSpecifierComparer, sourceFile, changeTracker)
|
||||
|
||||
if len(oldExportDecls) > 0 {
|
||||
if len(newExportDecls) == 0 {
|
||||
changeTracker.DeleteNodeRange(
|
||||
sourceFile,
|
||||
oldExportDecls[0].AsNode(),
|
||||
oldExportDecls[len(oldExportDecls)-1].AsNode(),
|
||||
change.LeadingTriviaOptionExclude,
|
||||
change.TrailingTriviaOptionInclude,
|
||||
)
|
||||
} else {
|
||||
for _, exp := range newExportDecls {
|
||||
changeTracker.AddEmitFlags(exp.AsNode(), printer.EFNoLeadingComments)
|
||||
}
|
||||
|
||||
options := change.NodeOptions{
|
||||
LeadingTriviaOption: change.LeadingTriviaOptionExclude,
|
||||
TrailingTriviaOption: change.TrailingTriviaOptionInclude,
|
||||
Suffix: "\n",
|
||||
}
|
||||
|
||||
newNodes := core.Map(newExportDecls, func(s *ast.Statement) *ast.Node { return s.AsNode() })
|
||||
changeTracker.ReplaceNodeWithNodes(sourceFile, oldExportDecls[0].AsNode(), newNodes, &options)
|
||||
|
||||
if len(oldExportDecls) > 1 {
|
||||
for i := 1; i < len(oldExportDecls); i++ {
|
||||
changeTracker.Delete(sourceFile, oldExportDecls[i].AsNode())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func coalesceExportsWorker(
|
||||
exportGroup []*ast.Statement,
|
||||
specifierComparer func(s1, s2 *ast.Node) int,
|
||||
moduleSpecifierComparer func(a, b string) int,
|
||||
sourceFile *ast.SourceFile,
|
||||
changeTracker *change.Tracker,
|
||||
) []*ast.Statement {
|
||||
if len(exportGroup) == 0 {
|
||||
return exportGroup
|
||||
}
|
||||
|
||||
exportsByModuleSpecifier := make(map[string][]*ast.Statement)
|
||||
var moduleSpecifierOrder []string
|
||||
|
||||
for _, exportDecl := range exportGroup {
|
||||
export := exportDecl.AsExportDeclaration()
|
||||
var moduleSpecifier string
|
||||
if export.ModuleSpecifier != nil {
|
||||
moduleSpecifier = export.ModuleSpecifier.Text()
|
||||
}
|
||||
if _, exists := exportsByModuleSpecifier[moduleSpecifier]; !exists {
|
||||
moduleSpecifierOrder = append(moduleSpecifierOrder, moduleSpecifier)
|
||||
}
|
||||
exportsByModuleSpecifier[moduleSpecifier] = append(exportsByModuleSpecifier[moduleSpecifier], exportDecl)
|
||||
}
|
||||
|
||||
slices.SortStableFunc(moduleSpecifierOrder, func(a, b string) int {
|
||||
if a == "" && b != "" {
|
||||
return 1
|
||||
}
|
||||
if a != "" && b == "" {
|
||||
return -1
|
||||
}
|
||||
return moduleSpecifierComparer(a, b)
|
||||
})
|
||||
|
||||
var coalescedExports []*ast.Statement
|
||||
factory := ast.NewNodeFactory(ast.NodeFactoryHooks{})
|
||||
|
||||
for _, moduleSpecifier := range moduleSpecifierOrder {
|
||||
group := exportsByModuleSpecifier[moduleSpecifier]
|
||||
|
||||
categorized := getCategorizedExports(group)
|
||||
|
||||
if categorized.exportWithoutClause != nil {
|
||||
coalescedExports = append(coalescedExports, categorized.exportWithoutClause)
|
||||
}
|
||||
|
||||
for _, subGroup := range [][]*ast.Statement{categorized.namedExports, categorized.typeOnlyExports} {
|
||||
if len(subGroup) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var newExportSpecifiers []*ast.Node
|
||||
for _, exportDecl := range subGroup {
|
||||
exportClause := exportDecl.AsExportDeclaration().ExportClause
|
||||
if exportClause != nil && exportClause.Kind == ast.KindNamedExports {
|
||||
namedExports := exportClause.AsNamedExports()
|
||||
newExportSpecifiers = append(newExportSpecifiers, namedExports.Elements.Nodes...)
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortStableFunc(newExportSpecifiers, specifierComparer)
|
||||
|
||||
exportDecl := subGroup[0].AsExportDeclaration()
|
||||
|
||||
var updatedExportClause *ast.NamedExportBindings
|
||||
if exportDecl.ExportClause != nil {
|
||||
if exportDecl.ExportClause.Kind == ast.KindNamedExports {
|
||||
namedExports := exportDecl.ExportClause.AsNamedExports()
|
||||
sortedList := factory.NewNodeList(newExportSpecifiers)
|
||||
updatedExportClause = factory.UpdateNamedExports(namedExports, sortedList)
|
||||
|
||||
if sourceFile != nil && !ast.NodeIsSynthesized(namedExports.AsNode()) && !printer.RangeIsOnSingleLine(namedExports.Loc, sourceFile) {
|
||||
changeTracker.SetEmitFlags(updatedExportClause.AsNode(), printer.EFMultiLine)
|
||||
}
|
||||
} else {
|
||||
updatedExportClause = exportDecl.ExportClause
|
||||
}
|
||||
}
|
||||
|
||||
newExportDecl := factory.UpdateExportDeclaration(
|
||||
exportDecl,
|
||||
exportDecl.Modifiers(),
|
||||
exportDecl.IsTypeOnly,
|
||||
updatedExportClause,
|
||||
exportDecl.ModuleSpecifier,
|
||||
exportDecl.Attributes,
|
||||
)
|
||||
coalescedExports = append(coalescedExports, newExportDecl)
|
||||
}
|
||||
}
|
||||
|
||||
return coalescedExports
|
||||
}
|
||||
|
||||
type categorizedExports struct {
|
||||
exportWithoutClause *ast.Statement
|
||||
namedExports []*ast.Statement
|
||||
typeOnlyExports []*ast.Statement
|
||||
}
|
||||
|
||||
func getCategorizedExports(exportGroup []*ast.Statement) categorizedExports {
|
||||
var exportWithoutClause *ast.Statement
|
||||
var namedExports, typeOnlyExports []*ast.Statement
|
||||
|
||||
for _, exportDecl := range exportGroup {
|
||||
export := exportDecl.AsExportDeclaration()
|
||||
if export.ExportClause == nil {
|
||||
if exportWithoutClause == nil {
|
||||
exportWithoutClause = exportDecl
|
||||
}
|
||||
} else if export.IsTypeOnly {
|
||||
typeOnlyExports = append(typeOnlyExports, exportDecl)
|
||||
} else {
|
||||
namedExports = append(namedExports, exportDecl)
|
||||
}
|
||||
}
|
||||
|
||||
return categorizedExports{
|
||||
exportWithoutClause: exportWithoutClause,
|
||||
namedExports: namedExports,
|
||||
typeOnlyExports: typeOnlyExports,
|
||||
}
|
||||
}
|
||||
379
tools/tsgo/internal/ls/rename.go
Normal file
379
tools/tsgo/internal/ls/rename.go
Normal file
@@ -0,0 +1,379 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"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/tspath"
|
||||
)
|
||||
|
||||
// RenameInfo represents the result of a rename validation check.
|
||||
// It is used by the `textDocument/prepareRename` LSP handler.
|
||||
type RenameInfo struct {
|
||||
CanRename bool
|
||||
LocalizedErrorMessage string
|
||||
DisplayName string
|
||||
TriggerSpan lsproto.Range
|
||||
FileToRename string
|
||||
NewFileName string
|
||||
}
|
||||
|
||||
func (l *LanguageService) ProvideRename(ctx context.Context, params *lsproto.RenameParams, orchestrator CrossProjectOrchestrator) (lsproto.WorkspaceEditOrNull, error) {
|
||||
return handleCrossProject(
|
||||
l,
|
||||
ctx,
|
||||
params,
|
||||
orchestrator,
|
||||
(*LanguageService).symbolAndEntriesToRename,
|
||||
combineRenameResponse,
|
||||
true, /*isRename*/
|
||||
false, /*implementations*/
|
||||
symbolEntryTransformOptions{},
|
||||
)
|
||||
}
|
||||
|
||||
func (l *LanguageService) GetRenameInfo(ctx context.Context, newName string, documentURI lsproto.DocumentUri, position lsproto.Position) RenameInfo {
|
||||
program, sourceFile := l.getProgramAndFile(documentURI)
|
||||
pos := int(l.converters.LineAndCharacterToPosition(sourceFile, position))
|
||||
|
||||
node := astnav.GetTouchingPropertyName(sourceFile, pos)
|
||||
node = getAdjustedLocation(node, true /*forRename*/, sourceFile)
|
||||
|
||||
if nodeIsEligibleForRename(node) {
|
||||
if renameInfo, ok := l.getRenameInfoForNode(ctx, newName, node, sourceFile, program); ok {
|
||||
return renameInfo
|
||||
}
|
||||
}
|
||||
return getRenameInfoError(ctx, diagnostics.You_cannot_rename_this_element)
|
||||
}
|
||||
|
||||
func (l *LanguageService) symbolAndEntriesToRename(ctx context.Context, params *lsproto.RenameParams, data SymbolAndEntriesData, options symbolEntryTransformOptions) (lsproto.WorkspaceEditOrNull, error) {
|
||||
if !nodeIsEligibleForRename(data.OriginalNode) {
|
||||
return lsproto.WorkspaceEditOrNull{}, nil
|
||||
}
|
||||
|
||||
program := l.GetProgram()
|
||||
|
||||
// Defense-in-depth: validate rename eligibility even if the client skipped prepareRename.
|
||||
// Use getRenameInfoForNode directly with the already-resolved node to avoid
|
||||
// re-resolving the position and polluting state baselines.
|
||||
sourceFile := ast.GetSourceFileOfNode(data.OriginalNode)
|
||||
if info, ok := l.getRenameInfoForNode(ctx, params.NewName, data.OriginalNode, sourceFile, program); !ok || !info.CanRename {
|
||||
return lsproto.WorkspaceEditOrNull{}, nil
|
||||
}
|
||||
|
||||
entries := core.FlatMap(data.SymbolsAndEntries, func(s *SymbolAndEntries) []*ReferenceEntry { return s.references })
|
||||
changes := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
|
||||
ch, done := program.GetTypeChecker(ctx)
|
||||
defer done()
|
||||
|
||||
quotePreference := lsutil.GetQuotePreference(sourceFile, l.UserPreferences())
|
||||
useAliasesForRename := l.UserPreferences().UseAliasesForRename.IsTrueOrUnknown()
|
||||
|
||||
for _, entry := range entries {
|
||||
uri := l.getFileNameOfEntry(entry)
|
||||
if l.UserPreferences().AllowRenameOfImportPath != core.TSTrue && entry.node != nil && ast.IsStringLiteralLike(entry.node) && ast.TryGetImportFromModuleSpecifier(entry.node) != nil {
|
||||
continue
|
||||
}
|
||||
textEdit := &lsproto.TextEdit{
|
||||
Range: l.getRangeOfEntry(entry),
|
||||
NewText: l.getTextForRename(data.OriginalNode, entry, params.NewName, ch, quotePreference, useAliasesForRename),
|
||||
}
|
||||
changes[uri] = append(changes[uri], textEdit)
|
||||
}
|
||||
return lsproto.WorkspaceEditOrNull{
|
||||
WorkspaceEdit: &lsproto.WorkspaceEdit{
|
||||
Changes: &changes,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getRenameInfoForNode performs detailed validation for a rename operation on a specific node.
|
||||
func (l *LanguageService) getRenameInfoForNode(ctx context.Context, newName string, node *ast.Node, sourceFile *ast.SourceFile, program *compiler.Program) (RenameInfo, bool) {
|
||||
ch, done := program.GetTypeChecker(ctx)
|
||||
defer done()
|
||||
|
||||
symbol := ch.GetSymbolAtLocation(node)
|
||||
if symbol == nil {
|
||||
if ast.IsStringLiteralLike(node) {
|
||||
// Allow renaming of string literal types with contextual string literal types
|
||||
typ := getContextualTypeFromParentOrAncestorTypeNode(node, ch)
|
||||
if typ != nil && (typ.IsStringLiteral() ||
|
||||
(typ.IsUnion() && core.Every(typ.Types(), func(t *checker.Type) bool {
|
||||
return t.IsStringLiteral()
|
||||
}))) {
|
||||
return getRenameInfoSuccess(node, sourceFile, node.Text(), l.converters), true
|
||||
}
|
||||
} else if ast.IsLabelName(node) {
|
||||
name := node.Text()
|
||||
return getRenameInfoSuccess(node, sourceFile, name, l.converters), true
|
||||
}
|
||||
return RenameInfo{}, false
|
||||
}
|
||||
|
||||
// Only allow a symbol to be renamed if it actually has at least one declaration.
|
||||
if len(symbol.Declarations) == 0 {
|
||||
return RenameInfo{}, false
|
||||
}
|
||||
|
||||
if msg := l.renameBlockedReason(sourceFile, node, symbol, ch, program); msg != nil {
|
||||
return getRenameInfoError(ctx, msg), true
|
||||
}
|
||||
|
||||
if ast.IsStringLiteralLike(node) && ast.TryGetImportFromModuleSpecifier(node) != nil {
|
||||
if l.UserPreferences().AllowRenameOfImportPath.IsTrue() {
|
||||
return l.getRenameInfoForModule(ctx, newName, node, sourceFile, symbol)
|
||||
}
|
||||
return RenameInfo{}, false
|
||||
}
|
||||
|
||||
return getRenameInfoSuccess(node, sourceFile, ch.SymbolToString(symbol), l.converters), true
|
||||
}
|
||||
|
||||
func nodeIsEligibleForRename(node *ast.Node) bool {
|
||||
switch node.Kind {
|
||||
case ast.KindIdentifier,
|
||||
ast.KindPrivateIdentifier,
|
||||
ast.KindStringLiteral,
|
||||
ast.KindNoSubstitutionTemplateLiteral,
|
||||
ast.KindThisKeyword:
|
||||
return true
|
||||
case ast.KindNumericLiteral:
|
||||
return isLiteralNameOfPropertyDeclarationOrIndexAccess(node)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// renameBlockedReason returns a non-nil diagnostic message if the rename should be blocked
|
||||
// because the symbol is a library definition, a default keyword, or would cross node_modules boundaries.
|
||||
func (l *LanguageService) renameBlockedReason(sourceFile *ast.SourceFile, node *ast.Node, symbol *ast.Symbol, ch *checker.Checker, program *compiler.Program) *diagnostics.Message {
|
||||
for _, declaration := range symbol.Declarations {
|
||||
if isDefinedInLibraryFile(program, declaration) {
|
||||
return diagnostics.You_cannot_rename_elements_that_are_defined_in_the_standard_TypeScript_library
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot rename `default` as in `import { default as foo } from "./someModule"`
|
||||
if ast.IsIdentifier(node) && node.Text() == "default" && symbol.Parent != nil && symbol.Parent.Flags&ast.SymbolFlagsModule != 0 {
|
||||
return diagnostics.You_cannot_rename_this_element
|
||||
}
|
||||
|
||||
if msg := wouldRenameInOtherNodeModules(sourceFile, symbol, ch, l.UserPreferences()); msg != nil {
|
||||
return msg
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isDefinedInLibraryFile checks if a declaration is from a default library file (e.g., lib.d.ts).
|
||||
func isDefinedInLibraryFile(program *compiler.Program, declaration *ast.Node) bool {
|
||||
declSourceFile := ast.GetSourceFileOfNode(declaration)
|
||||
return program.IsSourceFileDefaultLibrary(declSourceFile.Path()) && tspath.IsDeclarationFileName(declSourceFile.FileName())
|
||||
}
|
||||
|
||||
// wouldRenameInOtherNodeModules checks if renaming the symbol would affect node_modules.
|
||||
func wouldRenameInOtherNodeModules(originalFile *ast.SourceFile, symbol *ast.Symbol, ch *checker.Checker, preferences lsutil.UserPreferences) *diagnostics.Message {
|
||||
sym := symbol
|
||||
if !preferences.UseAliasesForRename.IsTrueOrUnknown() && sym.Flags&ast.SymbolFlagsAlias != 0 {
|
||||
importSpecifier := core.Find(sym.Declarations, ast.IsImportSpecifier)
|
||||
if importSpecifier != nil && importSpecifier.AsImportSpecifier().PropertyName == nil {
|
||||
sym = ch.GetAliasedSymbol(sym)
|
||||
}
|
||||
}
|
||||
|
||||
declarations := sym.Declarations
|
||||
if len(declarations) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
originalPackage := module.ParseNodeModuleFromPath(originalFile.FileName(), false /*isFolder*/)
|
||||
if originalPackage == "" {
|
||||
// Original source file is not in node_modules.
|
||||
for _, declaration := range declarations {
|
||||
if isInsideNodeModules(ast.GetSourceFileOfNode(declaration).FileName()) {
|
||||
return diagnostics.You_cannot_rename_elements_that_are_defined_in_a_node_modules_folder
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Original source file is in node_modules.
|
||||
for _, declaration := range declarations {
|
||||
declPackage := module.ParseNodeModuleFromPath(ast.GetSourceFileOfNode(declaration).FileName(), false /*isFolder*/)
|
||||
if declPackage != "" && declPackage != originalPackage {
|
||||
return diagnostics.You_cannot_rename_elements_that_are_defined_in_another_node_modules_folder
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClientSupportsWillRenameFiles(ctx context.Context) bool {
|
||||
return lsproto.GetClientCapabilities(ctx).Workspace.FileOperations.WillRename
|
||||
}
|
||||
|
||||
func ClientSupportsDocumentChanges(ctx context.Context) bool {
|
||||
return lsproto.GetClientCapabilities(ctx).Workspace.WorkspaceEdit.DocumentChanges
|
||||
}
|
||||
|
||||
func ClientSupportsRenameResourceOperations(ctx context.Context) bool {
|
||||
return slices.Contains(lsproto.GetClientCapabilities(ctx).Workspace.WorkspaceEdit.ResourceOperations, lsproto.ResourceOperationKindRename)
|
||||
}
|
||||
|
||||
// getRenameInfoForModule handles rename validation for module specifiers.
|
||||
func (l *LanguageService) getRenameInfoForModule(ctx context.Context, newName string, specifier *ast.StringLiteralLike, sourceFile *ast.SourceFile, moduleSymbol *ast.Symbol) (RenameInfo, bool) {
|
||||
if !tspath.IsExternalModuleNameRelative(specifier.Text()) {
|
||||
return getRenameInfoError(ctx, diagnostics.You_cannot_rename_a_module_via_a_global_import), true
|
||||
}
|
||||
if !ClientSupportsDocumentChanges(ctx) || !ClientSupportsRenameResourceOperations(ctx) {
|
||||
return getRenameInfoError(ctx, diagnostics.File_rename_is_not_supported_by_the_editor), true
|
||||
}
|
||||
|
||||
moduleSourceFile := core.Find(moduleSymbol.Declarations, ast.IsSourceFile)
|
||||
if moduleSourceFile == nil {
|
||||
return RenameInfo{}, false
|
||||
}
|
||||
|
||||
fileName := moduleSourceFile.AsSourceFile().FileName()
|
||||
withoutIndex := ""
|
||||
if !strings.HasSuffix(specifier.Text(), "/index") && !strings.HasSuffix(specifier.Text(), "/index.js") {
|
||||
candidate := tspath.RemoveFileExtension(fileName)
|
||||
if trimmed, ok := strings.CutSuffix(candidate, "/index"); ok {
|
||||
withoutIndex = trimmed
|
||||
}
|
||||
}
|
||||
|
||||
displayName := fileName
|
||||
if withoutIndex != "" {
|
||||
displayName = withoutIndex
|
||||
}
|
||||
newFileName := l.getNewFileNameForModuleRename(displayName, specifier.Text(), newName)
|
||||
|
||||
// Span should only be the last component of the path. + 1 to account for the quote character.
|
||||
indexAfterLastSlash := strings.LastIndex(specifier.Text(), "/") + 1
|
||||
start := specifier.Pos() + 1 + indexAfterLastSlash
|
||||
length := len(specifier.Text()) - indexAfterLastSlash
|
||||
|
||||
return RenameInfo{
|
||||
CanRename: true,
|
||||
DisplayName: specifier.Text()[indexAfterLastSlash:],
|
||||
TriggerSpan: l.converters.ToLSPRange(sourceFile, core.NewTextRange(start, start+length)),
|
||||
FileToRename: displayName,
|
||||
NewFileName: newFileName,
|
||||
}, true
|
||||
}
|
||||
|
||||
// Adjust the new name based on the old path that an import specifier resolves to.
|
||||
// For example, if specifier "a.js" resolves to file a.ts, renaming "a.js" -> "b.js" should mean file rename a.ts -> b.ts.
|
||||
func (l *LanguageService) getNewFileNameForModuleRename(oldPath, specifierText, newName string) string {
|
||||
newPath := tspath.CombinePaths(tspath.GetDirectoryPath(oldPath), newName)
|
||||
ignoreCase := !l.host.UseCaseSensitiveFileNames()
|
||||
var oldExt string
|
||||
if tspath.IsDeclarationFileName(oldPath) {
|
||||
oldExt = tspath.GetDeclarationFileExtension(oldPath)
|
||||
} else {
|
||||
oldExt = tspath.GetAnyExtensionFromPath(oldPath, nil /*extensions*/, ignoreCase)
|
||||
}
|
||||
if !tspath.HasExtension(newPath) {
|
||||
newPath = newPath + oldExt
|
||||
} else if tspath.GetAnyExtensionFromPath(newPath, nil /*extensions*/, ignoreCase) == tspath.GetAnyExtensionFromPath(specifierText, nil /*extensions*/, ignoreCase) {
|
||||
newPath = tspath.ChangeAnyExtension(newPath, oldExt, nil /*extensions*/, ignoreCase)
|
||||
}
|
||||
return newPath
|
||||
}
|
||||
|
||||
func (l *LanguageService) getTextForRename(originalNode *ast.Node, entry *ReferenceEntry, newText string, ch *checker.Checker, quotePreference lsutil.QuotePreference, useAliasesForRename bool) string {
|
||||
if useAliasesForRename && entry.kind != entryKindRange && (ast.IsIdentifier(originalNode) || ast.IsStringLiteralLike(originalNode)) {
|
||||
node := ast.GetReparsedNodeForNode(entry.node)
|
||||
kind := entry.kind
|
||||
parent := node.Parent
|
||||
name := originalNode.Text()
|
||||
isShorthandAssignment := ast.IsShorthandPropertyAssignment(parent)
|
||||
switch {
|
||||
case isShorthandAssignment || (isObjectBindingElementWithoutPropertyName(parent) && parent.Name() == node && parent.AsBindingElement().DotDotDotToken == nil):
|
||||
if kind == entryKindSearchedLocalFoundProperty {
|
||||
return name + ": " + newText
|
||||
}
|
||||
if kind == entryKindSearchedPropertyFoundLocal {
|
||||
return newText + ": " + name
|
||||
}
|
||||
// In `const o = { x }; o.x`, symbolAtLocation at `x` in `{ x }` is the property symbol.
|
||||
// For a binding element `const { x } = o;`, symbolAtLocation at `x` is the property symbol.
|
||||
if isShorthandAssignment {
|
||||
grandParent := parent.Parent
|
||||
if ast.IsObjectLiteralExpression(grandParent) && ast.IsBinaryExpression(grandParent.Parent) && ast.IsModuleExportsAccessExpression(grandParent.Parent.AsBinaryExpression().Left) {
|
||||
return name + ": " + newText
|
||||
}
|
||||
return newText + ": " + name
|
||||
}
|
||||
return name + ": " + newText
|
||||
case ast.IsImportSpecifier(parent) && parent.PropertyName() == nil:
|
||||
// If the original symbol was using this alias, just rename the alias.
|
||||
var originalSymbol *ast.Symbol
|
||||
if ast.IsExportSpecifier(originalNode.Parent) {
|
||||
originalSymbol = ch.GetExportSpecifierLocalTargetSymbol(originalNode.Parent)
|
||||
} else {
|
||||
originalSymbol = ch.GetSymbolAtLocation(originalNode)
|
||||
}
|
||||
if originalSymbol != nil && slices.Contains(originalSymbol.Declarations, parent) {
|
||||
return name + " as " + newText
|
||||
}
|
||||
return newText
|
||||
case ast.IsExportSpecifier(parent) && parent.PropertyName() == nil:
|
||||
// If the symbol for the node is same as declared node symbol use prefix text
|
||||
if originalNode == entry.node || ch.GetSymbolAtLocation(originalNode) == ch.GetSymbolAtLocation(entry.node) {
|
||||
return name + " as " + newText
|
||||
}
|
||||
return newText + " as " + name
|
||||
}
|
||||
}
|
||||
|
||||
// If the node is a numerical indexing literal, then add quotes around the property access.
|
||||
if entry.kind != entryKindRange && ast.IsNumericLiteral(entry.node) && ast.IsAccessExpression(entry.node.Parent) {
|
||||
quote := getQuoteFromPreference(quotePreference)
|
||||
return quote + newText + quote
|
||||
}
|
||||
|
||||
return newText
|
||||
}
|
||||
|
||||
func getQuoteFromPreference(quotePreference lsutil.QuotePreference) string {
|
||||
if quotePreference == lsutil.QuotePreferenceSingle {
|
||||
return "'"
|
||||
}
|
||||
return `"`
|
||||
}
|
||||
|
||||
func getRenameInfoError(ctx context.Context, message *diagnostics.Message) RenameInfo {
|
||||
return RenameInfo{
|
||||
CanRename: false,
|
||||
LocalizedErrorMessage: message.Localize(locale.FromContext(ctx)),
|
||||
}
|
||||
}
|
||||
|
||||
func getRenameInfoSuccess(node *ast.Node, sourceFile *ast.SourceFile, displayName string, converters *lsconv.Converters) RenameInfo {
|
||||
start := astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/)
|
||||
end := node.End()
|
||||
if ast.IsStringLiteralLike(node) {
|
||||
// Exclude the quotes
|
||||
start++
|
||||
end--
|
||||
}
|
||||
return RenameInfo{
|
||||
CanRename: true,
|
||||
DisplayName: displayName,
|
||||
TriggerSpan: converters.ToLSPRange(sourceFile, core.NewTextRange(start, end)),
|
||||
}
|
||||
}
|
||||
211
tools/tsgo/internal/ls/selectionranges.go
Normal file
211
tools/tsgo/internal/ls/selectionranges.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
)
|
||||
|
||||
func (l *LanguageService) ProvideSelectionRanges(ctx context.Context, params *lsproto.SelectionRangeParams) (lsproto.SelectionRangeResponse, error) {
|
||||
_, sourceFile := l.getProgramAndFile(params.TextDocument.Uri)
|
||||
if sourceFile == nil {
|
||||
return lsproto.SelectionRangesOrNull{}, nil
|
||||
}
|
||||
|
||||
var results []*lsproto.SelectionRange
|
||||
for _, position := range params.Positions {
|
||||
pos := l.converters.LineAndCharacterToPosition(sourceFile, position)
|
||||
selectionRange := getSmartSelectionRange(l, sourceFile, int(pos))
|
||||
if selectionRange != nil {
|
||||
results = append(results, selectionRange)
|
||||
}
|
||||
}
|
||||
|
||||
return lsproto.SelectionRangesOrNull{SelectionRanges: &results}, nil
|
||||
}
|
||||
|
||||
func getSmartSelectionRange(l *LanguageService, sourceFile *ast.SourceFile, pos int) *lsproto.SelectionRange {
|
||||
factory := &ast.NodeFactory{}
|
||||
|
||||
nodeContainsPosition := func(node *ast.Node) bool {
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
start := scanner.GetTokenPosOfNode(node, sourceFile, true /*includeJSDoc*/)
|
||||
end := node.End()
|
||||
return start <= pos && pos < end
|
||||
}
|
||||
|
||||
pushSelectionRange := func(current *lsproto.SelectionRange, start, end int) *lsproto.SelectionRange {
|
||||
if start == end {
|
||||
return current
|
||||
}
|
||||
|
||||
if !(start <= pos && pos <= end) {
|
||||
return current
|
||||
}
|
||||
|
||||
lspRange := l.converters.ToLSPRange(sourceFile, core.NewTextRange(start, end))
|
||||
|
||||
if current != nil && current.Range == lspRange {
|
||||
return current
|
||||
}
|
||||
|
||||
return &lsproto.SelectionRange{
|
||||
Range: lspRange,
|
||||
Parent: current,
|
||||
}
|
||||
}
|
||||
|
||||
pushSelectionCommentRange := func(current *lsproto.SelectionRange, start, end int) *lsproto.SelectionRange {
|
||||
current = pushSelectionRange(current, start, end)
|
||||
|
||||
commentPos := start
|
||||
text := sourceFile.Text()
|
||||
for commentPos < end && commentPos < len(text) && text[commentPos] == '/' {
|
||||
commentPos++
|
||||
}
|
||||
current = pushSelectionRange(current, commentPos, end)
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
positionsAreOnSameLine := func(pos1, pos2 int) bool {
|
||||
if pos1 == pos2 {
|
||||
return true
|
||||
}
|
||||
lspPos1 := l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(pos1))
|
||||
lspPos2 := l.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(pos2))
|
||||
return lspPos1.Line == lspPos2.Line
|
||||
}
|
||||
|
||||
shouldSkipNode := func(node *ast.Node, parent *ast.Node) bool {
|
||||
if ast.IsBlock(node) {
|
||||
return true
|
||||
}
|
||||
|
||||
if ast.IsTemplateSpan(node) || ast.IsTemplateHead(node) || ast.IsTemplateTail(node) {
|
||||
return true
|
||||
}
|
||||
|
||||
if parent != nil && ast.IsVariableDeclarationList(node) && ast.IsVariableStatement(parent) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip lone variable declarations
|
||||
if parent != nil && ast.IsVariableDeclaration(node) && ast.IsVariableDeclarationList(parent) {
|
||||
decl := parent.AsVariableDeclarationList()
|
||||
if decl != nil && len(decl.Declarations.Nodes) == 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if ast.IsJSDocTypeExpression(node) || ast.IsJSDocSignature(node) || ast.IsJSDocTypeLiteral(node) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fullRange := l.converters.ToLSPRange(sourceFile, core.NewTextRange(sourceFile.Pos(), sourceFile.End()))
|
||||
result := &lsproto.SelectionRange{
|
||||
Range: fullRange,
|
||||
}
|
||||
|
||||
var current *ast.Node
|
||||
for current = sourceFile.AsNode(); current != nil; {
|
||||
var next *ast.Node
|
||||
parent := current
|
||||
|
||||
visit := func(node *ast.Node) *ast.Node {
|
||||
if node != nil && next == nil {
|
||||
var foundComment *ast.CommentRange
|
||||
for comment := range scanner.GetTrailingCommentRanges(factory, sourceFile.Text(), node.End()) {
|
||||
foundComment = &comment
|
||||
break
|
||||
}
|
||||
if foundComment != nil && foundComment.Kind == ast.KindSingleLineCommentTrivia {
|
||||
result = pushSelectionCommentRange(result, foundComment.Pos(), foundComment.End())
|
||||
}
|
||||
|
||||
if nodeContainsPosition(node) {
|
||||
// Add range for multi-line function bodies before skipping the block
|
||||
if ast.IsBlock(node) && ast.IsFunctionLikeDeclaration(parent) {
|
||||
if !positionsAreOnSameLine(astnav.GetStartOfNode(node, sourceFile, false), node.End()) {
|
||||
start := astnav.GetStartOfNode(node, sourceFile, false)
|
||||
end := node.End()
|
||||
result = pushSelectionRange(result, start, end)
|
||||
}
|
||||
}
|
||||
|
||||
// Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings.
|
||||
if ast.IsTemplateSpan(parent) {
|
||||
templateSpan := parent.AsTemplateSpan()
|
||||
if templateSpan.Literal != nil {
|
||||
// Start from just before the '${' and end after the '}'
|
||||
// The '${' is 2 characters before the expression start
|
||||
spanStart := node.Pos() - 2
|
||||
// The '}' is the first character of the template literal (middle or tail)
|
||||
spanEnd := astnav.GetStartOfNode(templateSpan.Literal, sourceFile, false) + 1
|
||||
// Validate the positions are reasonable
|
||||
text := sourceFile.Text()
|
||||
if spanStart >= 0 && spanEnd <= len(text) && spanStart < spanEnd {
|
||||
result = pushSelectionRange(result, spanStart, spanEnd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldSkipNode(node, parent) {
|
||||
start := astnav.GetStartOfNode(node, sourceFile, false)
|
||||
end := node.End()
|
||||
result = pushSelectionRange(result, start, end)
|
||||
|
||||
// String literals should have a stop both inside and outside their quotes.
|
||||
if ast.IsStringLiteral(node) || node.Kind == ast.KindTemplateExpression || node.Kind == ast.KindNoSubstitutionTemplateLiteral {
|
||||
// Only add inner content range if there's actually content (handles unterminated literals)
|
||||
if start+1 < end-1 {
|
||||
result = pushSelectionRange(result, start+1, end-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next = node
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
visitNodes := func(nodes *ast.NodeList, v *ast.NodeVisitor) *ast.NodeList {
|
||||
if nodes != nil && len(nodes.Nodes) > 0 {
|
||||
shouldSkipList := parent != nil && (ast.IsVariableDeclarationList(parent) || ast.IsTemplateExpression(parent))
|
||||
|
||||
if !shouldSkipList {
|
||||
start := astnav.GetStartOfNode(nodes.Nodes[0], sourceFile, false)
|
||||
end := nodes.Nodes[len(nodes.Nodes)-1].End()
|
||||
|
||||
if start <= pos && pos < end {
|
||||
result = pushSelectionRange(result, start, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
return v.VisitNodes(nodes)
|
||||
}
|
||||
|
||||
// Visit JSDoc nodes first if they exist
|
||||
for _, jsdoc := range current.JSDoc(sourceFile) {
|
||||
visit(jsdoc)
|
||||
}
|
||||
|
||||
tempVisitor := ast.NewNodeVisitor(visit, nil, ast.NodeVisitorHooks{
|
||||
VisitNodes: visitNodes,
|
||||
})
|
||||
|
||||
current.VisitEachChild(tempVisitor)
|
||||
current = next
|
||||
}
|
||||
return result
|
||||
}
|
||||
576
tools/tsgo/internal/ls/semantictokens.go
Normal file
576
tools/tsgo/internal/ls/semantictokens.go
Normal file
@@ -0,0 +1,576 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
// tokenTypes defines the order of token types for encoding
|
||||
var tokenTypes = []lsproto.SemanticTokenType{
|
||||
lsproto.SemanticTokenTypeNamespace,
|
||||
lsproto.SemanticTokenTypeClass,
|
||||
lsproto.SemanticTokenTypeEnum,
|
||||
lsproto.SemanticTokenTypeInterface,
|
||||
lsproto.SemanticTokenTypeStruct,
|
||||
lsproto.SemanticTokenTypeTypeParameter,
|
||||
lsproto.SemanticTokenTypeType,
|
||||
lsproto.SemanticTokenTypeParameter,
|
||||
lsproto.SemanticTokenTypeVariable,
|
||||
lsproto.SemanticTokenTypeProperty,
|
||||
lsproto.SemanticTokenTypeEnumMember,
|
||||
lsproto.SemanticTokenTypeDecorator,
|
||||
lsproto.SemanticTokenTypeEvent,
|
||||
lsproto.SemanticTokenTypeFunction,
|
||||
lsproto.SemanticTokenTypeMethod,
|
||||
lsproto.SemanticTokenTypeMacro,
|
||||
lsproto.SemanticTokenTypeLabel,
|
||||
lsproto.SemanticTokenTypeComment,
|
||||
lsproto.SemanticTokenTypeString,
|
||||
lsproto.SemanticTokenTypeKeyword,
|
||||
lsproto.SemanticTokenTypeNumber,
|
||||
lsproto.SemanticTokenTypeRegexp,
|
||||
lsproto.SemanticTokenTypeOperator,
|
||||
}
|
||||
|
||||
// tokenModifiers defines the order of token modifiers for encoding
|
||||
var tokenModifiers = []lsproto.SemanticTokenModifier{
|
||||
lsproto.SemanticTokenModifierDeclaration,
|
||||
lsproto.SemanticTokenModifierDefinition,
|
||||
lsproto.SemanticTokenModifierReadonly,
|
||||
lsproto.SemanticTokenModifierStatic,
|
||||
lsproto.SemanticTokenModifierDeprecated,
|
||||
lsproto.SemanticTokenModifierAbstract,
|
||||
lsproto.SemanticTokenModifierAsync,
|
||||
lsproto.SemanticTokenModifierModification,
|
||||
lsproto.SemanticTokenModifierDocumentation,
|
||||
lsproto.SemanticTokenModifierDefaultLibrary,
|
||||
"local",
|
||||
}
|
||||
|
||||
type tokenType int
|
||||
|
||||
const (
|
||||
tokenTypeNamespace tokenType = iota
|
||||
tokenTypeClass
|
||||
tokenTypeEnum
|
||||
tokenTypeInterface
|
||||
tokenTypeStruct
|
||||
tokenTypeTypeParameter
|
||||
tokenTypeType
|
||||
tokenTypeParameter
|
||||
tokenTypeVariable
|
||||
tokenTypeProperty
|
||||
tokenTypeEnumMember
|
||||
tokenTypeDecorator
|
||||
tokenTypeEvent
|
||||
tokenTypeFunction
|
||||
tokenTypeMethod // Previously called "member" in TypeScript
|
||||
tokenTypeMacro
|
||||
tokenTypeLabel
|
||||
tokenTypeComment
|
||||
tokenTypeString
|
||||
tokenTypeKeyword
|
||||
tokenTypeNumber
|
||||
tokenTypeRegexp
|
||||
tokenTypeOperator
|
||||
)
|
||||
|
||||
type tokenModifier int
|
||||
|
||||
const (
|
||||
tokenModifierDeclaration tokenModifier = 1 << iota
|
||||
tokenModifierDefinition
|
||||
tokenModifierReadonly
|
||||
tokenModifierStatic
|
||||
tokenModifierDeprecated
|
||||
tokenModifierAbstract
|
||||
tokenModifierAsync
|
||||
tokenModifierModification
|
||||
tokenModifierDocumentation
|
||||
tokenModifierDefaultLibrary
|
||||
tokenModifierLocal
|
||||
)
|
||||
|
||||
// SemanticTokensLegend returns the legend describing the token types and modifiers.
|
||||
// It filters the legend to only include types and modifiers that the client supports,
|
||||
// as indicated by clientCapabilities.
|
||||
func SemanticTokensLegend(clientCapabilities lsproto.ResolvedSemanticTokensClientCapabilities) *lsproto.SemanticTokensLegend {
|
||||
types := make([]string, 0, len(tokenTypes))
|
||||
for _, t := range tokenTypes {
|
||||
if slices.Contains(clientCapabilities.TokenTypes, string(t)) {
|
||||
types = append(types, string(t))
|
||||
}
|
||||
}
|
||||
modifiers := make([]string, 0, len(tokenModifiers))
|
||||
for _, m := range tokenModifiers {
|
||||
if slices.Contains(clientCapabilities.TokenModifiers, string(m)) {
|
||||
modifiers = append(modifiers, string(m))
|
||||
}
|
||||
}
|
||||
return &lsproto.SemanticTokensLegend{
|
||||
TokenTypes: types,
|
||||
TokenModifiers: modifiers,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LanguageService) ProvideSemanticTokens(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.SemanticTokensResponse, error) {
|
||||
program, file := l.getProgramAndFile(documentURI)
|
||||
|
||||
c, done := program.GetTypeCheckerForFile(ctx, file)
|
||||
defer done()
|
||||
|
||||
tokens := l.collectSemanticTokens(ctx, c, file, program)
|
||||
|
||||
if len(tokens) == 0 {
|
||||
return lsproto.SemanticTokensOrNull{}, nil
|
||||
}
|
||||
|
||||
// Convert to LSP format (relative encoding)
|
||||
encoded := encodeSemanticTokens(ctx, tokens, file, l.converters)
|
||||
|
||||
return lsproto.SemanticTokensOrNull{
|
||||
SemanticTokens: &lsproto.SemanticTokens{
|
||||
Data: encoded,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *LanguageService) ProvideSemanticTokensRange(ctx context.Context, documentURI lsproto.DocumentUri, rng lsproto.Range) (lsproto.SemanticTokensRangeResponse, error) {
|
||||
program, file := l.getProgramAndFile(documentURI)
|
||||
|
||||
c, done := program.GetTypeCheckerForFile(ctx, file)
|
||||
defer done()
|
||||
|
||||
start := int(l.converters.LineAndCharacterToPosition(file, rng.Start))
|
||||
end := int(l.converters.LineAndCharacterToPosition(file, rng.End))
|
||||
|
||||
tokens := l.collectSemanticTokensInRange(ctx, c, file, program, start, end)
|
||||
|
||||
if len(tokens) == 0 {
|
||||
return lsproto.SemanticTokensOrNull{}, nil
|
||||
}
|
||||
|
||||
// Convert to LSP format (relative encoding)
|
||||
encoded := encodeSemanticTokens(ctx, tokens, file, l.converters)
|
||||
|
||||
return lsproto.SemanticTokensOrNull{
|
||||
SemanticTokens: &lsproto.SemanticTokens{
|
||||
Data: encoded,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type semanticToken struct {
|
||||
node *ast.Node
|
||||
tokenType tokenType
|
||||
tokenModifier tokenModifier
|
||||
}
|
||||
|
||||
func (l *LanguageService) collectSemanticTokens(ctx context.Context, c *checker.Checker, file *ast.SourceFile, program *compiler.Program) []semanticToken {
|
||||
return l.collectSemanticTokensInRange(ctx, c, file, program, file.Pos(), file.End())
|
||||
}
|
||||
|
||||
func (l *LanguageService) collectSemanticTokensInRange(ctx context.Context, c *checker.Checker, file *ast.SourceFile, program *compiler.Program, spanStart, spanEnd int) []semanticToken {
|
||||
tokens := []semanticToken{}
|
||||
|
||||
inJSXElement := false
|
||||
|
||||
var visit func(*ast.Node) bool
|
||||
visit = func(node *ast.Node) bool {
|
||||
// Check for cancellation
|
||||
if ctx.Err() != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
if node.Flags&ast.NodeFlagsReparsed != 0 {
|
||||
return false
|
||||
}
|
||||
nodeEnd := node.End()
|
||||
if node.Pos() >= spanEnd || nodeEnd <= spanStart {
|
||||
return false
|
||||
}
|
||||
|
||||
prevInJSXElement := inJSXElement
|
||||
if ast.IsJsxElement(node) || ast.IsJsxSelfClosingElement(node) {
|
||||
inJSXElement = true
|
||||
} else if ast.IsJsxExpression(node) {
|
||||
inJSXElement = false
|
||||
}
|
||||
|
||||
if ast.IsIdentifier(node) && node.Text() != "" && !inJSXElement && !isInImportClause(node) && !isInfinityOrNaNString(node.Text()) {
|
||||
symbol := c.GetSymbolAtLocation(node)
|
||||
if symbol != nil {
|
||||
// Resolve aliases
|
||||
if symbol.Flags&ast.SymbolFlagsAlias != 0 {
|
||||
symbol = c.GetAliasedSymbol(symbol)
|
||||
}
|
||||
|
||||
tokenType, ok := classifySymbol(symbol, getMeaningFromLocation(node))
|
||||
if ok {
|
||||
tokenModifier := tokenModifier(0)
|
||||
|
||||
// Check if this is a declaration
|
||||
parent := node.Parent
|
||||
if parent != nil {
|
||||
parentIsDeclaration := ast.IsBindingElement(parent) || tokenFromDeclarationMapping(parent.Kind) == tokenType
|
||||
if parentIsDeclaration && parent.Name() == node {
|
||||
tokenModifier |= tokenModifierDeclaration
|
||||
}
|
||||
}
|
||||
|
||||
// Property declaration in constructor: reclassify parameters as properties in property access context
|
||||
if tokenType == tokenTypeParameter && ast.IsRightSideOfQualifiedNameOrPropertyAccess(node) {
|
||||
tokenType = tokenTypeProperty
|
||||
}
|
||||
|
||||
// Type-based reclassification
|
||||
tokenType = reclassifyByType(c, node, tokenType)
|
||||
|
||||
// Get the value declaration to check modifiers
|
||||
if decl := symbol.ValueDeclaration; decl != nil {
|
||||
modifiers := ast.GetCombinedModifierFlags(decl)
|
||||
nodeFlags := ast.GetCombinedNodeFlags(decl)
|
||||
|
||||
if modifiers&ast.ModifierFlagsStatic != 0 {
|
||||
tokenModifier |= tokenModifierStatic
|
||||
}
|
||||
if modifiers&ast.ModifierFlagsAsync != 0 {
|
||||
tokenModifier |= tokenModifierAsync
|
||||
}
|
||||
if tokenType != tokenTypeClass && tokenType != tokenTypeInterface {
|
||||
if (modifiers&ast.ModifierFlagsReadonly != 0) || (nodeFlags&ast.NodeFlagsConst != 0) || (symbol.Flags&ast.SymbolFlagsEnumMember != 0) {
|
||||
tokenModifier |= tokenModifierReadonly
|
||||
}
|
||||
}
|
||||
if (tokenType == tokenTypeVariable || tokenType == tokenTypeFunction) && isLocalDeclaration(decl, file) {
|
||||
tokenModifier |= tokenModifierLocal
|
||||
}
|
||||
declSourceFile := ast.GetSourceFileOfNode(decl)
|
||||
if declSourceFile != nil && program.IsSourceFileDefaultLibrary(tspath.Path(declSourceFile.FileName())) {
|
||||
tokenModifier |= tokenModifierDefaultLibrary
|
||||
}
|
||||
} else if symbol.Declarations != nil {
|
||||
for _, decl := range symbol.Declarations {
|
||||
declSourceFile := ast.GetSourceFileOfNode(decl)
|
||||
if declSourceFile != nil && program.IsSourceFileDefaultLibrary(tspath.Path(declSourceFile.FileName())) {
|
||||
tokenModifier |= tokenModifierDefaultLibrary
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokens = append(tokens, semanticToken{
|
||||
node: node,
|
||||
tokenType: tokenType,
|
||||
tokenModifier: tokenModifier,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.ForEachChild(visit)
|
||||
inJSXElement = prevInJSXElement
|
||||
return false
|
||||
}
|
||||
|
||||
visit(file.AsNode())
|
||||
|
||||
// Check for cancellation after collection
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
func classifySymbol(symbol *ast.Symbol, meaning ast.SemanticMeaning) (tokenType, bool) {
|
||||
flags := symbol.Flags
|
||||
if flags&ast.SymbolFlagsClass != 0 {
|
||||
return tokenTypeClass, true
|
||||
}
|
||||
if flags&ast.SymbolFlagsEnum != 0 {
|
||||
return tokenTypeEnum, true
|
||||
}
|
||||
if flags&ast.SymbolFlagsTypeAlias != 0 {
|
||||
return tokenTypeType, true
|
||||
}
|
||||
if flags&ast.SymbolFlagsInterface != 0 {
|
||||
if meaning&ast.SemanticMeaningType != 0 {
|
||||
return tokenTypeInterface, true
|
||||
}
|
||||
}
|
||||
if flags&ast.SymbolFlagsTypeParameter != 0 {
|
||||
return tokenTypeTypeParameter, true
|
||||
}
|
||||
|
||||
// Check the value declaration
|
||||
decl := symbol.ValueDeclaration
|
||||
if decl == nil && len(symbol.Declarations) > 0 {
|
||||
decl = symbol.Declarations[0]
|
||||
}
|
||||
if decl != nil {
|
||||
if ast.IsBindingElement(decl) {
|
||||
decl = getDeclarationForBindingElement(decl)
|
||||
}
|
||||
if tokenType := tokenFromDeclarationMapping(decl.Kind); tokenType >= 0 {
|
||||
return tokenType, true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func tokenFromDeclarationMapping(kind ast.Kind) tokenType {
|
||||
switch kind {
|
||||
case ast.KindVariableDeclaration:
|
||||
return tokenTypeVariable
|
||||
case ast.KindParameter:
|
||||
return tokenTypeParameter
|
||||
case ast.KindPropertyDeclaration:
|
||||
return tokenTypeProperty
|
||||
case ast.KindModuleDeclaration:
|
||||
return tokenTypeNamespace
|
||||
case ast.KindEnumDeclaration:
|
||||
return tokenTypeEnum
|
||||
case ast.KindEnumMember:
|
||||
return tokenTypeEnumMember
|
||||
case ast.KindClassDeclaration, ast.KindClassExpression:
|
||||
return tokenTypeClass
|
||||
case ast.KindMethodDeclaration:
|
||||
return tokenTypeMethod
|
||||
case ast.KindFunctionDeclaration, ast.KindFunctionExpression:
|
||||
return tokenTypeFunction
|
||||
case ast.KindMethodSignature:
|
||||
return tokenTypeMethod
|
||||
case ast.KindGetAccessor, ast.KindSetAccessor:
|
||||
return tokenTypeProperty
|
||||
case ast.KindPropertySignature:
|
||||
return tokenTypeProperty
|
||||
case ast.KindInterfaceDeclaration:
|
||||
return tokenTypeInterface
|
||||
case ast.KindTypeAliasDeclaration:
|
||||
return tokenTypeType
|
||||
case ast.KindTypeParameter:
|
||||
return tokenTypeTypeParameter
|
||||
case ast.KindPropertyAssignment, ast.KindShorthandPropertyAssignment:
|
||||
return tokenTypeProperty
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
func reclassifyByType(c *checker.Checker, node *ast.Node, tt tokenType) tokenType {
|
||||
// Type-based reclassification for variables, properties, and parameters
|
||||
if tt == tokenTypeVariable || tt == tokenTypeProperty || tt == tokenTypeParameter {
|
||||
typ := c.GetTypeAtLocation(node)
|
||||
if typ != nil {
|
||||
test := func(condition func(*checker.Type) bool) bool {
|
||||
if condition(typ) {
|
||||
return true
|
||||
}
|
||||
if typ.Flags()&checker.TypeFlagsUnion != 0 {
|
||||
if slices.ContainsFunc(typ.AsUnionType().Types(), condition) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for constructor signatures (class-like)
|
||||
if tt != tokenTypeParameter && test(func(t *checker.Type) bool {
|
||||
return len(c.GetSignaturesOfType(t, checker.SignatureKindConstruct)) > 0
|
||||
}) {
|
||||
return tokenTypeClass
|
||||
}
|
||||
|
||||
// Check for call signatures (function-like)
|
||||
// Must have call signatures AND (no properties OR be used in call context)
|
||||
hasCallSignatures := test(func(t *checker.Type) bool {
|
||||
return len(c.GetSignaturesOfType(t, checker.SignatureKindCall)) > 0
|
||||
})
|
||||
if hasCallSignatures {
|
||||
hasNoProperties := !test(func(t *checker.Type) bool {
|
||||
objType := t.AsObjectType()
|
||||
return objType != nil && len(objType.Properties()) > 0
|
||||
})
|
||||
if hasNoProperties || isExpressionInCallExpression(node) {
|
||||
if tt == tokenTypeProperty {
|
||||
return tokenTypeMethod
|
||||
}
|
||||
return tokenTypeFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tt
|
||||
}
|
||||
|
||||
func isLocalDeclaration(decl *ast.Node, sourceFile *ast.SourceFile) bool {
|
||||
if ast.IsBindingElement(decl) {
|
||||
decl = getDeclarationForBindingElement(decl)
|
||||
}
|
||||
if ast.IsVariableDeclaration(decl) {
|
||||
parent := decl.Parent
|
||||
// Check if this is a catch clause parameter
|
||||
if parent != nil && ast.IsCatchClause(parent) {
|
||||
return ast.GetSourceFileOfNode(decl) == sourceFile
|
||||
}
|
||||
if parent != nil && ast.IsVariableDeclarationList(parent) {
|
||||
grandparent := parent.Parent
|
||||
if grandparent != nil {
|
||||
greatGrandparent := grandparent.Parent
|
||||
return (!ast.IsSourceFile(greatGrandparent) || ast.IsCatchClause(grandparent)) &&
|
||||
ast.GetSourceFileOfNode(decl) == sourceFile
|
||||
}
|
||||
}
|
||||
} else if ast.IsFunctionDeclaration(decl) {
|
||||
parent := decl.Parent
|
||||
return parent != nil && !ast.IsSourceFile(parent) && ast.GetSourceFileOfNode(decl) == sourceFile
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getDeclarationForBindingElement(element *ast.Node) *ast.Node {
|
||||
for {
|
||||
parent := element.Parent
|
||||
if parent != nil && ast.IsBindingPattern(parent) {
|
||||
grandparent := parent.Parent
|
||||
if grandparent != nil && ast.IsBindingElement(grandparent) {
|
||||
element = grandparent
|
||||
continue
|
||||
}
|
||||
return parent.Parent
|
||||
}
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
func isInImportClause(node *ast.Node) bool {
|
||||
parent := node.Parent
|
||||
return parent != nil && (ast.IsImportClause(parent) || ast.IsImportSpecifier(parent) || ast.IsNamespaceImport(parent))
|
||||
}
|
||||
|
||||
func isExpressionInCallExpression(node *ast.Node) bool {
|
||||
for ast.IsRightSideOfQualifiedNameOrPropertyAccess(node) {
|
||||
node = node.Parent
|
||||
}
|
||||
parent := node.Parent
|
||||
return parent != nil && ast.IsCallExpression(parent) && parent.Expression() == node
|
||||
}
|
||||
|
||||
func isInfinityOrNaNString(text string) bool {
|
||||
return text == "Infinity" || text == "NaN"
|
||||
}
|
||||
|
||||
// encodeSemanticTokens encodes tokens into the LSP format using relative positioning.
|
||||
// It filters tokens based on client capabilities, only including types and modifiers that the client supports.
|
||||
func encodeSemanticTokens(ctx context.Context, tokens []semanticToken, file *ast.SourceFile, converters *lsconv.Converters) []uint32 {
|
||||
// Build mapping from server token types/modifiers to client indices
|
||||
typeMapping := make(map[tokenType]uint32)
|
||||
modifierMapping := make(map[lsproto.SemanticTokenModifier]uint32)
|
||||
|
||||
clientCapabilities := lsproto.GetClientCapabilities(ctx).TextDocument.SemanticTokens
|
||||
|
||||
// Map server token types to client-supported indices
|
||||
clientIdx := uint32(0)
|
||||
for i, serverType := range tokenTypes {
|
||||
if slices.Contains(clientCapabilities.TokenTypes, string(serverType)) {
|
||||
typeMapping[tokenType(i)] = clientIdx
|
||||
clientIdx++
|
||||
}
|
||||
}
|
||||
|
||||
// Map server token modifiers to client-supported bit positions
|
||||
clientBit := uint32(0)
|
||||
for _, serverModifier := range tokenModifiers {
|
||||
if slices.Contains(clientCapabilities.TokenModifiers, string(serverModifier)) {
|
||||
modifierMapping[serverModifier] = clientBit
|
||||
clientBit++
|
||||
}
|
||||
}
|
||||
|
||||
// Each token encodes 5 uint32 values: deltaLine, deltaChar, length, tokenType, tokenModifiers
|
||||
encoded := make([]uint32, 0, len(tokens)*5)
|
||||
prevLine := uint32(0)
|
||||
prevChar := uint32(0)
|
||||
|
||||
for _, token := range tokens {
|
||||
// Skip tokens with types not supported by the client
|
||||
clientTypeIdx, typeSupported := typeMapping[token.tokenType]
|
||||
if !typeSupported {
|
||||
continue
|
||||
}
|
||||
|
||||
// Map modifiers to client-supported bit mask
|
||||
clientModifierMask := uint32(0)
|
||||
for i, serverModifier := range tokenModifiers {
|
||||
if token.tokenModifier&(1<<i) != 0 {
|
||||
if clientBit, ok := modifierMapping[serverModifier]; ok {
|
||||
clientModifierMask |= 1 << clientBit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use GetTokenPosOfNode to skip trivia (comments, whitespace) before the identifier
|
||||
tokenStart := scanner.GetTokenPosOfNode(token.node, file, false)
|
||||
tokenEnd := token.node.End()
|
||||
|
||||
// Convert both start and end positions to LSP coordinates, then compute length
|
||||
startPos := converters.PositionToLineAndCharacter(file, core.TextPos(tokenStart))
|
||||
endPos := converters.PositionToLineAndCharacter(file, core.TextPos(tokenEnd))
|
||||
|
||||
// Length is the character difference when on the same line
|
||||
var tokenLength uint32
|
||||
if startPos.Line == endPos.Line {
|
||||
tokenLength = endPos.Character - startPos.Character
|
||||
} else {
|
||||
panic(fmt.Sprintf("semantic tokens: token spans multiple lines: start=(%d,%d) end=(%d,%d) for token at offset %d",
|
||||
startPos.Line, startPos.Character, endPos.Line, endPos.Character, tokenStart))
|
||||
}
|
||||
|
||||
line := startPos.Line
|
||||
char := startPos.Character
|
||||
|
||||
// Verify that positions are strictly increasing (visitor walks in order)
|
||||
if len(encoded) > 0 && (line < prevLine || (line == prevLine && char <= prevChar)) {
|
||||
panic(fmt.Sprintf("semantic tokens: positions must be strictly increasing: prev=(%d,%d) current=(%d,%d) for token at offset %d",
|
||||
prevLine, prevChar, line, char, tokenStart))
|
||||
}
|
||||
|
||||
// Encode as: [deltaLine, deltaChar, length, tokenType, tokenModifiers]
|
||||
deltaLine := line - prevLine
|
||||
var deltaChar uint32
|
||||
if deltaLine == 0 {
|
||||
deltaChar = char - prevChar
|
||||
} else {
|
||||
deltaChar = char
|
||||
}
|
||||
|
||||
encoded = append(
|
||||
encoded,
|
||||
deltaLine,
|
||||
deltaChar,
|
||||
tokenLength,
|
||||
clientTypeIdx,
|
||||
clientModifierMask,
|
||||
)
|
||||
|
||||
prevLine = line
|
||||
prevChar = char
|
||||
}
|
||||
|
||||
return encoded
|
||||
}
|
||||
1421
tools/tsgo/internal/ls/signaturehelp.go
Normal file
1421
tools/tsgo/internal/ls/signaturehelp.go
Normal file
File diff suppressed because it is too large
Load Diff
134
tools/tsgo/internal/ls/source_map.go
Normal file
134
tools/tsgo/internal/ls/source_map.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/outputpaths"
|
||||
"github.com/microsoft/typescript-go/internal/sourcemap"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
func (l *LanguageService) getMappedLocation(fileName string, fileRange core.TextRange) lsproto.Location {
|
||||
startPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.Pos()))
|
||||
if startPos == nil {
|
||||
lspRange := l.createLspRangeFromRange(fileRange, l.getScript(fileName))
|
||||
return lsproto.Location{
|
||||
Uri: lsconv.FileNameToDocumentURI(fileName),
|
||||
Range: lspRange,
|
||||
}
|
||||
}
|
||||
endPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.End()))
|
||||
if endPos == nil || endPos.FileName != startPos.FileName || endPos.Pos < startPos.Pos {
|
||||
// When end doesn't map, maps to a different source file (e.g. in a .d.ts with a
|
||||
// multi-source source map from --outFile compilation), or maps to a position before
|
||||
// start (non-monotonic source map mappings), approximate the end position.
|
||||
endPos = &sourcemap.DocumentPosition{
|
||||
FileName: startPos.FileName,
|
||||
Pos: startPos.Pos + fileRange.Len(),
|
||||
}
|
||||
}
|
||||
newRange := core.NewTextRange(startPos.Pos, endPos.Pos)
|
||||
lspRange := l.createLspRangeFromRange(newRange, l.getScript(startPos.FileName))
|
||||
return lsproto.Location{
|
||||
Uri: lsconv.FileNameToDocumentURI(startPos.FileName),
|
||||
Range: lspRange,
|
||||
}
|
||||
}
|
||||
|
||||
type script struct {
|
||||
fileName string
|
||||
text string
|
||||
}
|
||||
|
||||
func (s *script) FileName() string {
|
||||
return s.fileName
|
||||
}
|
||||
|
||||
func (s *script) Text() string {
|
||||
return s.text
|
||||
}
|
||||
|
||||
func (l *LanguageService) getScript(fileName string) *script {
|
||||
text, ok := l.host.ReadFile(fileName)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &script{fileName: fileName, text: text}
|
||||
}
|
||||
|
||||
func (l *LanguageService) tryGetSourcePosition(
|
||||
fileName string,
|
||||
position core.TextPos,
|
||||
) *sourcemap.DocumentPosition {
|
||||
newPos := l.tryGetSourcePositionWorker(fileName, position)
|
||||
if newPos != nil {
|
||||
if _, ok := l.ReadFile(newPos.FileName); !ok { // File doesn't exist
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return newPos
|
||||
}
|
||||
|
||||
func (l *LanguageService) tryGetSourcePositionWorker(
|
||||
fileName string,
|
||||
position core.TextPos,
|
||||
) *sourcemap.DocumentPosition {
|
||||
if !tspath.IsDeclarationFileName(fileName) {
|
||||
return nil
|
||||
}
|
||||
|
||||
positionMapper := l.GetDocumentPositionMapper(fileName)
|
||||
documentPos := positionMapper.GetSourcePosition(&sourcemap.DocumentPosition{FileName: fileName, Pos: int(position)})
|
||||
if documentPos == nil {
|
||||
return nil
|
||||
}
|
||||
if newPos := l.tryGetSourcePositionWorker(documentPos.FileName, core.TextPos(documentPos.Pos)); newPos != nil {
|
||||
return newPos
|
||||
}
|
||||
return documentPos
|
||||
}
|
||||
|
||||
func (l *LanguageService) tryGetGeneratedPosition(
|
||||
fileName string,
|
||||
position core.TextPos,
|
||||
) *sourcemap.DocumentPosition {
|
||||
newPos := l.tryGetGeneratedPositionWorker(fileName, position)
|
||||
if newPos != nil {
|
||||
if _, ok := l.ReadFile(newPos.FileName); !ok { // File doesn't exist
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return newPos
|
||||
}
|
||||
|
||||
func (l *LanguageService) tryGetGeneratedPositionWorker(
|
||||
fileName string,
|
||||
position core.TextPos,
|
||||
) *sourcemap.DocumentPosition {
|
||||
if tspath.IsDeclarationFileName(fileName) {
|
||||
return nil
|
||||
}
|
||||
|
||||
program := l.GetProgram()
|
||||
if program == nil || program.GetSourceFile(fileName) == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := l.toPath(fileName)
|
||||
// If this is source file of project reference source (instead of redirect) there is no generated position
|
||||
if program.IsSourceFromProjectReference(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
declarationFileName := outputpaths.GetOutputDeclarationFileNameWorker(fileName, program.Options(), program)
|
||||
positionMapper := l.GetDocumentPositionMapper(declarationFileName)
|
||||
documentPos := positionMapper.GetGeneratedPosition(&sourcemap.DocumentPosition{FileName: fileName, Pos: int(position)})
|
||||
if documentPos == nil {
|
||||
return nil
|
||||
}
|
||||
if newPos := l.tryGetGeneratedPositionWorker(documentPos.FileName, core.TextPos(documentPos.Pos)); newPos != nil {
|
||||
return newPos
|
||||
}
|
||||
return documentPos
|
||||
}
|
||||
707
tools/tsgo/internal/ls/sourcedefinition.go
Normal file
707
tools/tsgo/internal/ls/sourcedefinition.go
Normal file
@@ -0,0 +1,707 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/binder"
|
||||
"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/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/module"
|
||||
"github.com/microsoft/typescript-go/internal/modulespecifiers"
|
||||
"github.com/microsoft/typescript-go/internal/parser"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
func (l *LanguageService) ProvideSourceDefinition(
|
||||
ctx context.Context,
|
||||
documentURI lsproto.DocumentUri,
|
||||
position lsproto.Position,
|
||||
) (lsproto.DefinitionResponse, error) {
|
||||
caps := lsproto.GetClientCapabilities(ctx)
|
||||
clientSupportsLink := caps.TextDocument.Definition.LinkSupport
|
||||
|
||||
program, file := l.getProgramAndFile(documentURI)
|
||||
pos := int(l.converters.LineAndCharacterToPosition(file, position))
|
||||
resolver := l.newSourceDefResolver(program, file.FileName())
|
||||
node := astnav.GetTouchingPropertyName(file, pos)
|
||||
|
||||
if node.Kind == ast.KindSourceFile {
|
||||
// Triple-slash directives are comments, not AST nodes, so
|
||||
// GetTouchingPropertyName returns the SourceFile node.
|
||||
if declarations, ref := resolver.resolveTripleSlashReference(file, pos, program); len(declarations) != 0 {
|
||||
originSelectionRange := l.createLspRangeFromBounds(ref.Pos(), ref.End(), file)
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, declarations, nil /*reference*/), nil
|
||||
}
|
||||
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil
|
||||
}
|
||||
|
||||
originSelectionRange := l.createLspRangeFromNode(node, file)
|
||||
|
||||
// If the cursor is directly on a module specifier string, resolve to the
|
||||
// implementation file's entry point.
|
||||
containingModuleSpecifier := findContainingModuleSpecifier(node)
|
||||
if node == containingModuleSpecifier {
|
||||
specifierMode := program.GetModeForUsageLocation(file, containingModuleSpecifier)
|
||||
if implementationFile := resolver.resolveImplementation(containingModuleSpecifier.Text(), specifierMode); implementationFile != "" {
|
||||
if sourceFile := resolver.getOrParseSourceFile(implementationFile); sourceFile != nil {
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, getSourceDefinitionEntryDeclarations(sourceFile), nil), nil
|
||||
}
|
||||
}
|
||||
return l.provideDefinitionWorker(ctx, documentURI, position)
|
||||
}
|
||||
|
||||
// Phase 1: Syntactic fast path — when the cursor is inside an
|
||||
// import/require/export, forward-resolve the module specifier to an
|
||||
// implementation file and search it directly. This avoids acquiring
|
||||
// the type checker entirely when the fast path succeeds.
|
||||
var resolvedImplFile string
|
||||
if containingModuleSpecifier != nil {
|
||||
specifierMode := program.GetModeForUsageLocation(file, containingModuleSpecifier)
|
||||
resolvedImplFile = resolver.resolveImplementation(containingModuleSpecifier.Text(), specifierMode)
|
||||
}
|
||||
|
||||
if resolvedImplFile != "" {
|
||||
names := getCandidateSourceDeclarationNames(node, nil)
|
||||
moduleResults := resolver.searchImplementationFile(node, resolvedImplFile, names)
|
||||
if len(moduleResults) != 0 {
|
||||
if !ast.IsPartOfTypeNode(node) && !ast.IsPartOfTypeOnlyImportOrExportDeclaration(node) || hasConcreteSourceDeclarations(moduleResults) {
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, uniqueDeclarationNodes(moduleResults), nil), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Type checker path — acquire the checker for the original file
|
||||
// and use its declarations and module specifier to map to source
|
||||
// implementations. This is the only point where the checker is used;
|
||||
// after this, only the NoDts module resolver and file parsing are needed.
|
||||
checkerDeclarations, moduleSpecifier := getSourceDefCheckerInfo(ctx, program, file, node)
|
||||
|
||||
// Phase 3: Map checker results to source definitions.
|
||||
declarations := resolver.resolveFromCheckerInfo(node, resolvedImplFile, checkerDeclarations, moduleSpecifier)
|
||||
if len(declarations) == 0 {
|
||||
// If we resolved an implementation file from an import/export but
|
||||
// couldn't find specific declarations, fall back to the file entry
|
||||
// point rather than the standard definition provider — unless the
|
||||
// checker found declarations that are all type-only (e.g. interfaces),
|
||||
// in which case the .d.ts definition is more appropriate.
|
||||
if containingModuleSpecifier != nil && resolvedImplFile != "" && !hasConcreteSourceDeclarations(checkerDeclarations) {
|
||||
if sourceFile := resolver.getOrParseSourceFile(resolvedImplFile); sourceFile != nil {
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, getSourceDefinitionEntryDeclarations(sourceFile), nil), nil
|
||||
}
|
||||
}
|
||||
return l.provideDefinitionWorker(ctx, documentURI, position)
|
||||
}
|
||||
return l.createDefinitionLocations(originSelectionRange, clientSupportsLink, declarations, nil /*reference*/), nil
|
||||
}
|
||||
|
||||
// sourceDefResolver resolves source definitions by mapping .d.ts declarations
|
||||
// to their implementation files (.js/.ts). It uses the NoDts module resolver
|
||||
// and file parsing for resolution, but never acquires the type checker or
|
||||
// the original program; all checker-dependent work is done before results
|
||||
// are passed in.
|
||||
type sourceDefResolver struct {
|
||||
ls *LanguageService
|
||||
fs vfs.FS
|
||||
options *core.CompilerOptions
|
||||
getSourceFile func(string) *ast.SourceFile
|
||||
resolveFrom string
|
||||
resolver *module.Resolver
|
||||
parsedFiles map[string]*ast.SourceFile
|
||||
}
|
||||
|
||||
func (l *LanguageService) newSourceDefResolver(
|
||||
program *compiler.Program,
|
||||
resolveFrom string,
|
||||
) *sourceDefResolver {
|
||||
options := program.Options()
|
||||
noDtsOptions := options.Clone()
|
||||
noDtsOptions.NoDtsResolution = core.TSTrue
|
||||
return &sourceDefResolver{
|
||||
ls: l,
|
||||
fs: program.Host().FS(),
|
||||
options: options,
|
||||
getSourceFile: program.GetSourceFile,
|
||||
resolveFrom: resolveFrom,
|
||||
resolver: module.NewResolver(program.Host(), noDtsOptions, program.GetGlobalTypingsCacheLocation(), ""),
|
||||
}
|
||||
}
|
||||
|
||||
// resolveFromCheckerInfo maps type-checker declarations to source
|
||||
// implementations. It uses only the NoDts module resolver and file parsing;
|
||||
// the type checker and original request file are not needed.
|
||||
func (r *sourceDefResolver) resolveFromCheckerInfo(
|
||||
node *ast.Node,
|
||||
resolvedImplFile string,
|
||||
checkerDeclarations []*ast.Node,
|
||||
moduleSpecifier string,
|
||||
) []*ast.Node {
|
||||
// If we don't yet have a forward-resolved implementation file, try to
|
||||
// recover a module specifier from the checker (e.g. from the import that
|
||||
// brought the symbol into scope, or from the root of an access expression).
|
||||
if resolvedImplFile == "" && moduleSpecifier != "" {
|
||||
resolvedImplFile = r.resolveImplementation(moduleSpecifier, r.inferImpliedNodeFormat(r.resolveFrom))
|
||||
}
|
||||
|
||||
// For property access where the checker found no declarations (e.g.
|
||||
// mapped types), search the implementation file for the property name.
|
||||
if len(checkerDeclarations) == 0 && resolvedImplFile != "" {
|
||||
names := getCandidateSourceDeclarationNames(node, nil)
|
||||
if results := r.searchImplementationFile(node, resolvedImplFile, names); results != nil {
|
||||
return uniqueDeclarationNodes(results)
|
||||
}
|
||||
}
|
||||
|
||||
var declarations []*ast.Node
|
||||
for _, declaration := range checkerDeclarations {
|
||||
declarations = append(declarations, r.mapDeclarationToSource(node, declaration, resolvedImplFile)...)
|
||||
}
|
||||
declarations = uniqueDeclarationNodes(declarations)
|
||||
if hasConcreteSourceDeclarations(declarations) {
|
||||
return declarations
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSourceDefCheckerInfo acquires the type checker for the given file and
|
||||
// returns the definition declarations for node along with the module specifier
|
||||
// of the import that brought the symbol into scope (empty if not applicable).
|
||||
func getSourceDefCheckerInfo(
|
||||
ctx context.Context,
|
||||
program *compiler.Program,
|
||||
file *ast.SourceFile,
|
||||
node *ast.Node,
|
||||
) ([]*ast.Node, string) {
|
||||
c, done := program.GetTypeCheckerForFile(ctx, file)
|
||||
defer done()
|
||||
|
||||
declarations := getDeclarationsFromLocation(c, node)
|
||||
isPropertyName := node.Parent != nil && ast.IsAccessExpression(node.Parent) && node.Parent.Name() == node
|
||||
if len(declarations) == 0 && isPropertyName {
|
||||
if left := node.Parent.Expression(); left != nil {
|
||||
if prop := c.GetPropertyOfType(c.GetTypeAtLocation(left), node.Text()); prop != nil {
|
||||
declarations = prop.Declarations
|
||||
}
|
||||
}
|
||||
}
|
||||
if calledDeclaration := tryGetSignatureDeclaration(c, node); calledDeclaration != nil {
|
||||
nonFunctionDeclarations := core.Filter(declarations, func(node *ast.Node) bool { return !ast.IsFunctionLike(node) })
|
||||
declarations = append(nonFunctionDeclarations, calledDeclaration)
|
||||
}
|
||||
|
||||
// Extract module specifier from the import that brought this symbol into
|
||||
// scope. For property access (obj.prop), walk up the access chain to the
|
||||
// root expression's symbol.
|
||||
var moduleSpecifier string
|
||||
resolveNode := node
|
||||
if isPropertyName {
|
||||
expr := node.Parent.Expression()
|
||||
for expr != nil && ast.IsAccessExpression(expr) {
|
||||
expr = expr.Expression()
|
||||
}
|
||||
if expr != nil {
|
||||
resolveNode = expr
|
||||
}
|
||||
}
|
||||
if sym := c.GetSymbolAtLocation(resolveNode); sym != nil {
|
||||
for _, d := range sym.Declarations {
|
||||
if !ast.IsImportSpecifier(d) && !ast.IsImportClause(d) && !ast.IsNamespaceImport(d) && !ast.IsImportEqualsDeclaration(d) {
|
||||
continue
|
||||
}
|
||||
if spec := checker.TryGetModuleSpecifierFromDeclaration(d); spec != nil {
|
||||
moduleSpecifier = spec.Text()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return declarations, moduleSpecifier
|
||||
}
|
||||
|
||||
// resolveTripleSlashReference handles /// <reference path/types="..."/> directives.
|
||||
// For path references to .js files, it returns the entry declarations directly.
|
||||
// For path references to .d.ts files or type references, it uses the NoDts
|
||||
// resolver to find the corresponding implementation file.
|
||||
func (r *sourceDefResolver) resolveTripleSlashReference(file *ast.SourceFile, pos int, program *compiler.Program) ([]*ast.Node, *ast.FileReference) {
|
||||
ref := getReferenceAtPosition(file, pos, program)
|
||||
if ref == nil || ref.file == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If the referenced file is already an implementation file, return it directly.
|
||||
if !ref.file.IsDeclarationFile {
|
||||
return getSourceDefinitionEntryDeclarations(ref.file), ref.reference
|
||||
}
|
||||
|
||||
// The referenced file is a .d.ts. Try to find the implementation file
|
||||
// using the NoDts module resolver via findImplementationFileFromDtsFileName.
|
||||
dtsFileName := ref.file.FileName()
|
||||
preferredMode := r.inferImpliedNodeFormat(dtsFileName)
|
||||
implementationFile := r.findImplementationFileFromDtsFileName(dtsFileName, preferredMode)
|
||||
if implementationFile == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sourceFile := r.getOrParseSourceFile(implementationFile)
|
||||
if sourceFile == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return getSourceDefinitionEntryDeclarations(sourceFile), ref.reference
|
||||
}
|
||||
|
||||
// searchImplementationFile searches an implementation file for declarations
|
||||
// matching the given names. Returns nil when no declarations matched; callers
|
||||
// fall through to the checker path or to the standard definition provider.
|
||||
func (r *sourceDefResolver) searchImplementationFile(
|
||||
originalNode *ast.Node,
|
||||
implementationFile string,
|
||||
names []string,
|
||||
) []*ast.Node {
|
||||
if implementationFile == "" {
|
||||
return nil
|
||||
}
|
||||
sourceFile := r.getOrParseSourceFile(implementationFile)
|
||||
if sourceFile == nil {
|
||||
return nil
|
||||
}
|
||||
if isDefaultImportName(originalNode) {
|
||||
// For default imports, only search for "default" declarations to avoid
|
||||
// matching unrelated declarations with the same identifier name.
|
||||
defaultDeclarations := r.findDeclarationsInFile(implementationFile, []string{"default"}, &collections.Set[string]{})
|
||||
if len(defaultDeclarations) != 0 {
|
||||
return filterPreferredSourceDeclarations(originalNode, defaultDeclarations)
|
||||
}
|
||||
return getSourceDefinitionEntryDeclarations(sourceFile)
|
||||
}
|
||||
declarations := r.findDeclarationsInFile(implementationFile, names, &collections.Set[string]{})
|
||||
if len(declarations) != 0 {
|
||||
return filterPreferredSourceDeclarations(originalNode, declarations)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDefaultImportName(node *ast.Node) bool {
|
||||
if node == nil || node.Parent == nil || !ast.IsImportClause(node.Parent) || node.Parent.Name() != node || node.Parent.Parent == nil {
|
||||
return false
|
||||
}
|
||||
return ast.IsDefaultImport(node.Parent.Parent)
|
||||
}
|
||||
|
||||
func getSourceDefinitionEntryNode(sourceFile *ast.SourceFile) *ast.Node {
|
||||
if len(sourceFile.Statements.Nodes) != 0 {
|
||||
return sourceFile.Statements.Nodes[0].AsNode()
|
||||
}
|
||||
return sourceFile.AsNode()
|
||||
}
|
||||
|
||||
func getSourceDefinitionEntryDeclarations(sourceFile *ast.SourceFile) []*ast.Node {
|
||||
return []*ast.Node{getSourceDefinitionEntryNode(sourceFile)}
|
||||
}
|
||||
|
||||
func (r *sourceDefResolver) mapDeclarationToSource(
|
||||
originalNode *ast.Node,
|
||||
declaration *ast.Node,
|
||||
resolvedImplFile string,
|
||||
) []*ast.Node {
|
||||
file, startPos := getFileAndStartPosFromDeclaration(declaration)
|
||||
fileName := file.FileName()
|
||||
|
||||
if mapped := r.ls.tryGetSourcePosition(fileName, startPos); mapped != nil {
|
||||
if sourceFile := r.getOrParseSourceFile(mapped.FileName); sourceFile != nil {
|
||||
return []*ast.Node{findClosestDeclarationNode(sourceFile, mapped.Pos)}
|
||||
}
|
||||
}
|
||||
|
||||
if !tspath.IsDeclarationFileName(fileName) {
|
||||
return []*ast.Node{declaration}
|
||||
}
|
||||
|
||||
implementationFile := resolvedImplFile
|
||||
if implementationFile == "" {
|
||||
// Reverse-resolve .d.ts path to implementation file. This path is only
|
||||
// reached for declarations with no associated module specifier (e.g.
|
||||
// globals, ambient declarations, or when forward resolution failed).
|
||||
dtsFileName := ast.GetSourceFileOfNode(declaration).FileName()
|
||||
preferredMode := r.inferImpliedNodeFormat(dtsFileName)
|
||||
implementationFile = r.findImplementationFileFromDtsFileName(dtsFileName, preferredMode)
|
||||
}
|
||||
|
||||
return r.searchImplementationFile(originalNode, implementationFile, getCandidateSourceDeclarationNames(originalNode, declaration))
|
||||
}
|
||||
|
||||
func (r *sourceDefResolver) findImplementationFileFromDtsFileName(
|
||||
dtsFileName string,
|
||||
preferredMode core.ResolutionMode,
|
||||
) string {
|
||||
if jsExt := module.TryGetJSExtensionForFile(dtsFileName, r.options); jsExt != "" {
|
||||
candidate := tspath.ChangeExtension(dtsFileName, jsExt)
|
||||
if r.fs.FileExists(candidate) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
parts := modulespecifiers.GetNodeModulePathParts(dtsFileName)
|
||||
if parts == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Ensure the file only contains one /node_modules/ segment. If there's more
|
||||
// than one, the package name extraction may be incorrect, so bail out.
|
||||
if strings.LastIndex(dtsFileName, "/node_modules/") != parts.TopLevelNodeModulesIndex {
|
||||
return ""
|
||||
}
|
||||
|
||||
packageNamePathPart := dtsFileName[parts.TopLevelPackageNameIndex+1 : parts.PackageRootIndex]
|
||||
packageName := module.GetPackageNameFromTypesPackageName(module.UnmangleScopedPackageName(packageNamePathPart))
|
||||
if packageName == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
pathToFileInPackage := dtsFileName[parts.PackageRootIndex+1:]
|
||||
|
||||
// Try resolving as a package subpath first (e.g. "pkg/dist/utils"), then
|
||||
// fall back to the bare package name (e.g. "pkg"). This covers both main
|
||||
// entrypoints and deep imports without needing to inspect package.json
|
||||
// entrypoints.
|
||||
if pathToFileInPackage != "" {
|
||||
specifier := packageName + "/" + tspath.RemoveFileExtension(pathToFileInPackage)
|
||||
if implementationFile := r.resolveImplementation(specifier, preferredMode); implementationFile != "" {
|
||||
return implementationFile
|
||||
}
|
||||
}
|
||||
return r.resolveImplementation(packageName, preferredMode)
|
||||
}
|
||||
|
||||
func (r *sourceDefResolver) resolveImplementation(
|
||||
moduleName string,
|
||||
preferredMode core.ResolutionMode,
|
||||
) string {
|
||||
return r.resolveImplementationFrom(moduleName, r.resolveFrom, preferredMode)
|
||||
}
|
||||
|
||||
func (r *sourceDefResolver) resolveImplementationFrom(
|
||||
moduleName string,
|
||||
resolveFromFile string,
|
||||
preferredMode core.ResolutionMode,
|
||||
) string {
|
||||
modes := []core.ResolutionMode{preferredMode}
|
||||
if preferredMode != core.ModuleKindESNext {
|
||||
modes = append(modes, core.ModuleKindESNext)
|
||||
}
|
||||
if preferredMode != core.ModuleKindCommonJS {
|
||||
modes = append(modes, core.ModuleKindCommonJS)
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
resolved, _ := r.resolver.ResolveModuleName(moduleName, resolveFromFile, mode, nil)
|
||||
if resolved != nil && resolved.IsResolved() && !tspath.IsDeclarationFileName(resolved.ResolvedFileName) {
|
||||
return resolved.ResolvedFileName
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *sourceDefResolver) getOrParseSourceFile(fileName string) *ast.SourceFile {
|
||||
if sourceFile := r.getSourceFile(fileName); sourceFile != nil {
|
||||
return sourceFile
|
||||
}
|
||||
if sourceFile, ok := r.parsedFiles[fileName]; ok {
|
||||
return sourceFile
|
||||
}
|
||||
var sourceFile *ast.SourceFile
|
||||
if text, ok := r.ls.ReadFile(fileName); ok {
|
||||
sourceFile = parser.ParseSourceFile(
|
||||
ast.SourceFileParseOptions{FileName: fileName, Path: r.ls.toPath(fileName)},
|
||||
text,
|
||||
core.GetScriptKindFromFileName(fileName),
|
||||
)
|
||||
binder.BindSourceFile(sourceFile)
|
||||
}
|
||||
if r.parsedFiles == nil {
|
||||
r.parsedFiles = map[string]*ast.SourceFile{}
|
||||
}
|
||||
r.parsedFiles[fileName] = sourceFile
|
||||
return sourceFile
|
||||
}
|
||||
|
||||
// inferImpliedNodeFormat determines the module format for a source file that may not be
|
||||
// in the program, using the file extension and nearest package.json "type" field.
|
||||
func (r *sourceDefResolver) inferImpliedNodeFormat(fileName string) core.ResolutionMode {
|
||||
var packageJsonType string
|
||||
if scope := r.resolver.GetPackageScopeForPath(tspath.GetDirectoryPath(fileName)); scope.Exists() {
|
||||
if value, ok := scope.Contents.Type.GetValue(); ok {
|
||||
packageJsonType = value
|
||||
}
|
||||
}
|
||||
return ast.GetImpliedNodeFormatForFile(fileName, packageJsonType)
|
||||
}
|
||||
|
||||
func findContainingModuleSpecifier(node *ast.Node) *ast.Node {
|
||||
for current := node; current != nil; current = current.Parent {
|
||||
if ast.IsAnyImportOrReExport(current) || ast.IsRequireCall(current, true /*requireStringLiteralLikeArgument*/) || ast.IsImportCall(current) {
|
||||
if moduleSpecifier := ast.GetExternalModuleName(current); moduleSpecifier != nil && ast.IsStringLiteralLike(moduleSpecifier) {
|
||||
return moduleSpecifier
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sourceDefResolver) findDeclarationsInFile(
|
||||
fileName string,
|
||||
names []string,
|
||||
seen *collections.Set[string],
|
||||
) []*ast.Node {
|
||||
if fileName == "" || len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
if !seen.AddIfAbsent(fileName) {
|
||||
return nil
|
||||
}
|
||||
|
||||
sourceFile := r.getOrParseSourceFile(fileName)
|
||||
if sourceFile == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
declarations := findDeclarationNodesByName(sourceFile, names)
|
||||
if len(declarations) != 0 && hasConcreteSourceDeclarations(declarations) {
|
||||
return declarations
|
||||
}
|
||||
|
||||
var forwarded []*ast.Node
|
||||
for _, forwardedFile := range r.getForwardedImplementationFiles(sourceFile) {
|
||||
forwarded = append(forwarded, r.findDeclarationsInFile(forwardedFile, names, seen)...)
|
||||
}
|
||||
if len(forwarded) != 0 {
|
||||
if hasConcreteSourceDeclarations(forwarded) {
|
||||
return uniqueDeclarationNodes(forwarded)
|
||||
}
|
||||
return uniqueDeclarationNodes(append(slices.Clip(declarations), forwarded...))
|
||||
}
|
||||
return declarations
|
||||
}
|
||||
|
||||
func (r *sourceDefResolver) getForwardedImplementationFiles(sourceFile *ast.SourceFile) []string {
|
||||
preferredMode := r.inferImpliedNodeFormat(sourceFile.FileName())
|
||||
|
||||
var files []string
|
||||
for _, imp := range sourceFile.Imports() {
|
||||
moduleName := imp.Text()
|
||||
if implementationFile := r.resolveImplementationFrom(moduleName, sourceFile.FileName(), preferredMode); implementationFile != "" {
|
||||
files = append(files, implementationFile)
|
||||
}
|
||||
}
|
||||
return core.Deduplicate(files)
|
||||
}
|
||||
|
||||
func getCandidateSourceDeclarationNames(originalNode *ast.Node, declaration *ast.Node) []string {
|
||||
var names []string
|
||||
if declaration != nil {
|
||||
if name := ast.GetNameOfDeclaration(declaration); name != nil {
|
||||
if text := ast.GetTextOfPropertyName(name); text != "" {
|
||||
names = append(names, text)
|
||||
}
|
||||
}
|
||||
if declaration.Kind == ast.KindExportAssignment {
|
||||
names = append(names, "default")
|
||||
}
|
||||
if (ast.IsFunctionDeclaration(declaration) || ast.IsClassDeclaration(declaration)) && declaration.ModifierFlags()&ast.ModifierFlagsExportDefault == ast.ModifierFlagsExportDefault {
|
||||
names = append(names, "default")
|
||||
}
|
||||
if ast.IsImportSpecifier(declaration) || ast.IsExportSpecifier(declaration) {
|
||||
if propName := declaration.PropertyName(); propName != nil {
|
||||
names = append(names, propName.Text())
|
||||
}
|
||||
}
|
||||
}
|
||||
if originalNode != nil {
|
||||
if ast.IsIdentifier(originalNode) || ast.IsPrivateIdentifier(originalNode) {
|
||||
names = append(names, originalNode.Text())
|
||||
}
|
||||
if isDefaultImportName(originalNode) {
|
||||
names = append(names, "default")
|
||||
}
|
||||
if originalNode.Parent != nil {
|
||||
if ast.IsImportSpecifier(originalNode.Parent) || ast.IsExportSpecifier(originalNode.Parent) {
|
||||
if propName := originalNode.Parent.PropertyName(); propName != nil {
|
||||
names = append(names, propName.Text())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func findDeclarationNodesByName(sourceFile *ast.SourceFile, names []string) []*ast.Node {
|
||||
names = core.Deduplicate(core.Filter(names, func(name string) bool { return name != "" }))
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var wanted collections.Set[string]
|
||||
wantDefault := false
|
||||
for _, name := range names {
|
||||
if name == "default" {
|
||||
wantDefault = true
|
||||
continue
|
||||
}
|
||||
wanted.Add(name)
|
||||
}
|
||||
|
||||
type candidate struct {
|
||||
node *ast.Node
|
||||
depth int
|
||||
}
|
||||
var candidates []candidate
|
||||
minDepth := math.MaxInt
|
||||
|
||||
var visit ast.Visitor
|
||||
visit = func(node *ast.Node) bool {
|
||||
matched := false
|
||||
if name := ast.GetNameOfDeclaration(node); name != nil {
|
||||
if text := ast.GetTextOfPropertyName(name); text != "" {
|
||||
if wanted.Has(text) {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if wantDefault && node.Kind == ast.KindExportAssignment {
|
||||
matched = true
|
||||
}
|
||||
if wantDefault && (ast.IsFunctionDeclaration(node) || ast.IsClassDeclaration(node)) && node.ModifierFlags()&ast.ModifierFlagsExportDefault == ast.ModifierFlagsExportDefault {
|
||||
matched = true
|
||||
}
|
||||
if matched {
|
||||
depth := getContainerDepth(node)
|
||||
candidates = append(candidates, candidate{node: node, depth: depth})
|
||||
if depth < minDepth {
|
||||
minDepth = depth
|
||||
}
|
||||
}
|
||||
return node.ForEachChild(visit)
|
||||
}
|
||||
sourceFile.AsNode().ForEachChild(visit)
|
||||
|
||||
// Only keep declarations at the shallowest depth, like getTopMostDeclarationNamesInFile.
|
||||
var declarations []*ast.Node
|
||||
for _, c := range candidates {
|
||||
if c.depth == minDepth {
|
||||
declarations = append(declarations, c.node)
|
||||
}
|
||||
}
|
||||
return uniqueDeclarationNodes(declarations)
|
||||
}
|
||||
|
||||
// getContainerDepth counts the number of container nodes above a declaration,
|
||||
// matching the behavior of getDepth in getTopMostDeclarationNamesInFile.
|
||||
func getContainerDepth(node *ast.Node) int {
|
||||
depth := 0
|
||||
current := node
|
||||
for current != nil {
|
||||
current = getContainerNode(current)
|
||||
depth++
|
||||
}
|
||||
return depth
|
||||
}
|
||||
|
||||
func filterPreferredSourceDeclarations(originalNode *ast.Node, declarations []*ast.Node) []*ast.Node {
|
||||
if len(declarations) <= 1 || originalNode == nil {
|
||||
return declarations
|
||||
}
|
||||
if preferred := getPropertyLikeSourceDeclarations(originalNode, declarations); len(preferred) != 0 {
|
||||
return preferred
|
||||
}
|
||||
if preferred := core.Filter(declarations, isConcreteSourceDeclaration); len(preferred) != 0 {
|
||||
return preferred
|
||||
}
|
||||
return declarations
|
||||
}
|
||||
|
||||
func getPropertyLikeSourceDeclarations(originalNode *ast.Node, declarations []*ast.Node) []*ast.Node {
|
||||
if originalNode.Parent == nil || !ast.IsAccessExpression(originalNode.Parent) || originalNode.Parent.Name() != originalNode {
|
||||
return nil
|
||||
}
|
||||
return core.Filter(declarations, func(node *ast.Node) bool {
|
||||
switch node.Kind {
|
||||
case ast.KindPropertyAssignment,
|
||||
ast.KindShorthandPropertyAssignment,
|
||||
ast.KindPropertyDeclaration,
|
||||
ast.KindPropertySignature,
|
||||
ast.KindMethodDeclaration,
|
||||
ast.KindMethodSignature,
|
||||
ast.KindGetAccessor,
|
||||
ast.KindSetAccessor,
|
||||
ast.KindEnumMember:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func hasConcreteSourceDeclarations(declarations []*ast.Node) bool {
|
||||
return slices.ContainsFunc(declarations, isConcreteSourceDeclaration)
|
||||
}
|
||||
|
||||
func isConcreteSourceDeclaration(node *ast.Node) bool {
|
||||
if !ast.IsDeclaration(node) || node.Kind == ast.KindExportAssignment {
|
||||
return false
|
||||
}
|
||||
if (ast.IsBinaryExpression(node) || ast.IsCallExpression(node)) && ast.GetAssignmentDeclarationKind(node) != ast.JSDeclarationKindNone {
|
||||
return false
|
||||
}
|
||||
switch node.Kind {
|
||||
case ast.KindParameter,
|
||||
ast.KindTypeParameter,
|
||||
ast.KindBindingElement,
|
||||
ast.KindImportClause,
|
||||
ast.KindImportSpecifier,
|
||||
ast.KindNamespaceImport,
|
||||
ast.KindExportSpecifier,
|
||||
ast.KindPropertyAccessExpression,
|
||||
ast.KindElementAccessExpression:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueDeclarationNodes(nodes []*ast.Node) []*ast.Node {
|
||||
type declarationKey struct {
|
||||
fileName string
|
||||
loc core.TextRange
|
||||
}
|
||||
var seen collections.Set[declarationKey]
|
||||
result := make([]*ast.Node, 0, len(nodes))
|
||||
for _, node := range nodes {
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
fileName := ast.GetSourceFileOfNode(node).FileName()
|
||||
key := declarationKey{fileName: fileName, loc: node.Loc}
|
||||
if !seen.AddIfAbsent(key) {
|
||||
continue
|
||||
}
|
||||
result = append(result, node)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findClosestDeclarationNode(sourceFile *ast.SourceFile, pos int) *ast.Node {
|
||||
node := astnav.GetTouchingPropertyName(sourceFile, pos)
|
||||
for current := node; current != nil; current = current.Parent {
|
||||
if ast.IsDeclaration(current) || current.Kind == ast.KindExportAssignment {
|
||||
return current
|
||||
}
|
||||
}
|
||||
return getSourceDefinitionEntryNode(sourceFile)
|
||||
}
|
||||
2211
tools/tsgo/internal/ls/string_completions.go
Normal file
2211
tools/tsgo/internal/ls/string_completions.go
Normal file
File diff suppressed because it is too large
Load Diff
694
tools/tsgo/internal/ls/symbols.go
Normal file
694
tools/tsgo/internal/ls/symbols.go
Normal file
@@ -0,0 +1,694 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"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/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/printer"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
func (l *LanguageService) ProvideDocumentSymbols(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.DocumentSymbolResponse, error) {
|
||||
_, file := l.getProgramAndFile(documentURI)
|
||||
if lsproto.GetClientCapabilities(ctx).TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport {
|
||||
symbols := l.getDocumentSymbolsForChildren(ctx, file.AsNode(), file)
|
||||
return lsproto.SymbolInformationsOrDocumentSymbolsOrNull{DocumentSymbols: &symbols}, nil
|
||||
}
|
||||
// Client doesn't support hierarchical document symbols, return flat SymbolInformation array
|
||||
symbolInfos := l.getDocumentSymbolInformations(ctx, file, documentURI)
|
||||
symbolInfoPtrs := make([]*lsproto.SymbolInformation, len(symbolInfos))
|
||||
for i := range symbolInfos {
|
||||
symbolInfoPtrs[i] = &symbolInfos[i]
|
||||
}
|
||||
return lsproto.SymbolInformationsOrDocumentSymbolsOrNull{SymbolInformations: &symbolInfoPtrs}, nil
|
||||
}
|
||||
|
||||
// getDocumentSymbolInformations converts hierarchical DocumentSymbols to a flat SymbolInformation array
|
||||
func (l *LanguageService) getDocumentSymbolInformations(ctx context.Context, file *ast.SourceFile, documentURI lsproto.DocumentUri) []lsproto.SymbolInformation {
|
||||
// First get hierarchical symbols
|
||||
docSymbols := l.getDocumentSymbolsForChildren(ctx, file.AsNode(), file)
|
||||
|
||||
// Flatten the hierarchy
|
||||
var result []lsproto.SymbolInformation
|
||||
var flatten func(symbols []*lsproto.DocumentSymbol, containerName *string)
|
||||
flatten = func(symbols []*lsproto.DocumentSymbol, containerName *string) {
|
||||
for _, symbol := range symbols {
|
||||
info := lsproto.SymbolInformation{
|
||||
Name: symbol.Name,
|
||||
Kind: symbol.Kind,
|
||||
Location: lsproto.Location{
|
||||
Uri: documentURI,
|
||||
Range: symbol.Range,
|
||||
},
|
||||
ContainerName: containerName,
|
||||
Tags: symbol.Tags,
|
||||
Deprecated: symbol.Deprecated,
|
||||
}
|
||||
result = append(result, info)
|
||||
|
||||
// Recursively flatten children with this symbol as container
|
||||
if symbol.Children != nil && len(*symbol.Children) > 0 {
|
||||
flatten(*symbol.Children, &symbol.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
flatten(docSymbols, nil)
|
||||
return result
|
||||
}
|
||||
|
||||
func (l *LanguageService) getDocumentSymbolsForChildren(ctx context.Context, node *ast.Node, file *ast.SourceFile) []*lsproto.DocumentSymbol {
|
||||
var symbols []*lsproto.DocumentSymbol
|
||||
expandoTargets := collections.Set[string]{}
|
||||
addSymbolForNode := func(node *ast.Node, name *ast.Node, children []*lsproto.DocumentSymbol) {
|
||||
if node.Flags&ast.NodeFlagsReparsed == 0 {
|
||||
symbol := l.newDocumentSymbol(node, name, children)
|
||||
if symbol != nil {
|
||||
symbols = append(symbols, symbol)
|
||||
}
|
||||
}
|
||||
}
|
||||
var visit func(*ast.Node) bool
|
||||
getSymbolsForChildren := func(node *ast.Node) []*lsproto.DocumentSymbol {
|
||||
var result []*lsproto.DocumentSymbol
|
||||
if node != nil {
|
||||
saveExpandoTargets := expandoTargets
|
||||
expandoTargets = collections.Set[string]{}
|
||||
saveSymbols := symbols
|
||||
symbols = nil
|
||||
node.ForEachChild(visit)
|
||||
result = symbols
|
||||
symbols = saveSymbols
|
||||
expandoTargets = saveExpandoTargets
|
||||
}
|
||||
return result
|
||||
}
|
||||
startNode := func(node *ast.Node, name *ast.Node) func() {
|
||||
if node == nil {
|
||||
return func() {}
|
||||
}
|
||||
saveExpandoTargets := expandoTargets
|
||||
expandoTargets = collections.Set[string]{}
|
||||
saveSymbols := symbols
|
||||
symbols = nil
|
||||
return func() {
|
||||
result := symbols
|
||||
symbols = saveSymbols
|
||||
expandoTargets = saveExpandoTargets
|
||||
addSymbolForNode(node, name, result)
|
||||
}
|
||||
}
|
||||
getSymbolsForNode := func(node *ast.Node) []*lsproto.DocumentSymbol {
|
||||
var result []*lsproto.DocumentSymbol
|
||||
if node != nil {
|
||||
saveSymbols := symbols
|
||||
symbols = nil
|
||||
visit(node)
|
||||
result = symbols
|
||||
symbols = saveSymbols
|
||||
}
|
||||
return result
|
||||
}
|
||||
visit = func(node *ast.Node) bool {
|
||||
if ctx.Err() != nil {
|
||||
return true
|
||||
}
|
||||
if node.Flags&ast.NodeFlagsReparsed == 0 {
|
||||
if jsdocs := node.JSDoc(file); len(jsdocs) > 0 {
|
||||
for _, jsdoc := range jsdocs {
|
||||
if tagList := jsdoc.AsJSDoc().Tags; tagList != nil {
|
||||
for _, tag := range tagList.Nodes {
|
||||
if ast.IsJSDocTypedefTag(tag) || ast.IsJSDocCallbackTag(tag) {
|
||||
addSymbolForNode(tag, nil /*name*/, nil /*children*/)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
switch node.Kind {
|
||||
case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration:
|
||||
if ast.IsClassLike(node) && ast.GetDeclarationName(node) != "" {
|
||||
expandoTargets.Add(ast.GetDeclarationName(node))
|
||||
}
|
||||
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node))
|
||||
case ast.KindModuleDeclaration:
|
||||
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(getInteriorModule(node)))
|
||||
case ast.KindConstructor:
|
||||
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node.Body()))
|
||||
for _, param := range node.Parameters() {
|
||||
if ast.IsParameterPropertyDeclaration(param, node) {
|
||||
addSymbolForNode(param, nil /*name*/, nil /*children*/)
|
||||
}
|
||||
}
|
||||
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindMethodDeclaration, ast.KindGetAccessor,
|
||||
ast.KindSetAccessor:
|
||||
declName := ast.GetDeclarationName(node)
|
||||
if declName != "" {
|
||||
expandoTargets.Add(declName)
|
||||
}
|
||||
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node.Body()))
|
||||
case ast.KindVariableDeclaration, ast.KindBindingElement, ast.KindPropertyAssignment, ast.KindPropertyDeclaration:
|
||||
nodeName := node.Name()
|
||||
if nodeName != nil {
|
||||
if ast.IsBindingPattern(nodeName) {
|
||||
visit(nodeName)
|
||||
} else {
|
||||
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node.Initializer()))
|
||||
}
|
||||
}
|
||||
case ast.KindSpreadAssignment:
|
||||
addSymbolForNode(node, node.Expression(), nil /*children*/)
|
||||
case ast.KindMethodSignature, ast.KindPropertySignature, ast.KindCallSignature, ast.KindConstructSignature, ast.KindIndexSignature,
|
||||
ast.KindEnumMember, ast.KindShorthandPropertyAssignment, ast.KindTypeAliasDeclaration, ast.KindImportEqualsDeclaration, ast.KindExportSpecifier:
|
||||
addSymbolForNode(node, nil /*name*/, nil /*children*/)
|
||||
case ast.KindImportClause:
|
||||
// Handle default import case e.g.:
|
||||
// import d from "mod";
|
||||
if node.Name() != nil {
|
||||
addSymbolForNode(node.Name(), node.Name(), nil /*children*/)
|
||||
}
|
||||
// Handle named bindings in imports e.g.:
|
||||
// import * as NS from "mod";
|
||||
// import {a, b as B} from "mod";
|
||||
if namedBindings := node.AsImportClause().NamedBindings; namedBindings != nil {
|
||||
if namedBindings.Kind == ast.KindNamespaceImport {
|
||||
addSymbolForNode(namedBindings, nil /*name*/, nil /*children*/)
|
||||
} else {
|
||||
for _, element := range namedBindings.Elements() {
|
||||
addSymbolForNode(element, nil /*name*/, nil /*children*/)
|
||||
}
|
||||
}
|
||||
}
|
||||
case ast.KindBinaryExpression, ast.KindCallExpression:
|
||||
assignmentKind := ast.GetAssignmentDeclarationKind(node)
|
||||
switch assignmentKind {
|
||||
// `module.exports = ...`` should be reparsed into a JSExportAssignment,
|
||||
// and `exports.a = ...`` into a CommonJSExport.
|
||||
case ast.JSDeclarationKindNone, ast.JSDeclarationKindThisProperty,
|
||||
ast.JSDeclarationKindModuleExports, ast.JSDeclarationKindExportsProperty,
|
||||
ast.JSDeclarationKindObjectDefinePropertyExports:
|
||||
node.ForEachChild(visit)
|
||||
case ast.JSDeclarationKindProperty, ast.JSDeclarationKindObjectDefinePropertyValue:
|
||||
var target *ast.Expression
|
||||
var targetFunction *ast.Expression
|
||||
var definition *ast.Node
|
||||
var propertyName *ast.Node
|
||||
// `A.b = ... ` or `A.prototype.b = ...`
|
||||
if ast.IsBinaryExpression(node) {
|
||||
binaryExpr := node.AsBinaryExpression()
|
||||
target = binaryExpr.Left
|
||||
targetFunction = target.Expression()
|
||||
definition = binaryExpr.Right
|
||||
// `A.b` or `A.prototype.b`
|
||||
if ast.IsPropertyAccessExpression(target) {
|
||||
propertyName = target.AsPropertyAccessExpression().Name()
|
||||
} else { // `A["b"]` or `A.prototype["b"]`
|
||||
propertyName = target.AsElementAccessExpression().ArgumentExpression
|
||||
}
|
||||
} else { // `Object.defineProperty(A, "b", {...})`
|
||||
args := node.Arguments()
|
||||
targetFunction = args[0]
|
||||
target = args[1]
|
||||
propertyName = target
|
||||
definition = args[2]
|
||||
}
|
||||
if isPrototypeExpando(targetFunction) {
|
||||
targetFunction = targetFunction.Expression()
|
||||
// If we see a prototype assignment, start tracking the target as an expando target.
|
||||
if ast.IsIdentifier(targetFunction) {
|
||||
expandoTargets.Add(targetFunction.Text())
|
||||
}
|
||||
}
|
||||
if ast.IsIdentifier(targetFunction) &&
|
||||
expandoTargets.Has(targetFunction.Text()) {
|
||||
endNode := startNode(node, targetFunction)
|
||||
addSymbolForNode(target, propertyName, getSymbolsForNode(definition))
|
||||
endNode()
|
||||
} else {
|
||||
node.ForEachChild(visit)
|
||||
}
|
||||
}
|
||||
case ast.KindExportAssignment:
|
||||
if node.AsExportAssignment().IsExportEquals {
|
||||
addSymbolForNode(node, nil /*name*/, getSymbolsForNode(node.Expression()))
|
||||
} else {
|
||||
node.ForEachChild(visit)
|
||||
}
|
||||
default:
|
||||
node.ForEachChild(visit)
|
||||
}
|
||||
return false
|
||||
}
|
||||
node.ForEachChild(visit)
|
||||
return mergeExpandos(symbols)
|
||||
}
|
||||
|
||||
// Target is `f.prototype`.
|
||||
func isPrototypeExpando(target *ast.Node) bool {
|
||||
if ast.IsAccessExpression(target) {
|
||||
accessName := ast.GetElementOrPropertyAccessName(target)
|
||||
return accessName != nil && accessName.Text() == "prototype"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const maxLength = 150
|
||||
|
||||
func (l *LanguageService) newDocumentSymbol(node *ast.Node, name *ast.Node, children []*lsproto.DocumentSymbol) *lsproto.DocumentSymbol {
|
||||
result := new(lsproto.DocumentSymbol)
|
||||
file := ast.GetSourceFileOfNode(node)
|
||||
nodeStartPos := scanner.SkipTrivia(file.Text(), node.Pos())
|
||||
if name == nil {
|
||||
name = ast.GetNameOfDeclaration(node)
|
||||
}
|
||||
var text string
|
||||
var nameStartPos, nameEndPos int
|
||||
if ast.IsModuleDeclaration(node) && !ast.IsAmbientModule(node) {
|
||||
text = getModuleName(node)
|
||||
nameStartPos = scanner.SkipTrivia(file.Text(), name.Pos())
|
||||
nameEndPos = getInteriorModule(node).Name().End()
|
||||
} else if ast.IsAnyExportAssignment(node) && node.AsExportAssignment().IsExportEquals {
|
||||
text = "export="
|
||||
if !ast.NodeIsMissing(name) {
|
||||
nameStartPos = scanner.SkipTrivia(file.Text(), name.Pos())
|
||||
nameEndPos = name.End()
|
||||
} else {
|
||||
nameStartPos = nodeStartPos
|
||||
nameEndPos = node.End()
|
||||
}
|
||||
} else if name != nil {
|
||||
text = getTextOfName(name)
|
||||
nameStartPos = max(scanner.SkipTrivia(file.Text(), name.Pos()), nodeStartPos)
|
||||
nameEndPos = max(name.End(), nodeStartPos)
|
||||
} else {
|
||||
text = getUnnamedNodeLabel(node)
|
||||
nameStartPos = nodeStartPos
|
||||
nameEndPos = nodeStartPos
|
||||
}
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
truncatedText := stringutil.TruncateByRunes(text, maxLength)
|
||||
if len(truncatedText) < len(text) {
|
||||
text = truncatedText + "..."
|
||||
}
|
||||
result.Name = text
|
||||
result.Kind = getSymbolKindFromNode(node)
|
||||
result.Range = lsproto.Range{
|
||||
Start: l.converters.PositionToLineAndCharacter(file, core.TextPos(nodeStartPos)),
|
||||
End: l.converters.PositionToLineAndCharacter(file, core.TextPos(node.End())),
|
||||
}
|
||||
result.SelectionRange = lsproto.Range{
|
||||
Start: l.converters.PositionToLineAndCharacter(file, core.TextPos(nameStartPos)),
|
||||
End: l.converters.PositionToLineAndCharacter(file, core.TextPos(nameEndPos)),
|
||||
}
|
||||
if children == nil {
|
||||
children = []*lsproto.DocumentSymbol{}
|
||||
}
|
||||
result.Children = &children
|
||||
return result
|
||||
}
|
||||
|
||||
// Merges expando symbols into their target symbols, and namespaces of same name.
|
||||
// Modifies the input slice.
|
||||
func mergeExpandos(symbols []*lsproto.DocumentSymbol) []*lsproto.DocumentSymbol {
|
||||
mergedSymbols := make([]*lsproto.DocumentSymbol, 0, len(symbols))
|
||||
// Collect symbols that can be an expando target.
|
||||
nameToExpandoTargetIndex := collections.MultiMap[string, int]{}
|
||||
// Collect namespaces.
|
||||
nameToNamespaceIndex := map[string]int{}
|
||||
for i, symbol := range symbols {
|
||||
if isAnonymousName(symbol.Name) {
|
||||
continue
|
||||
}
|
||||
if symbol.Kind == lsproto.SymbolKindClass || symbol.Kind == lsproto.SymbolKindFunction || symbol.Kind == lsproto.SymbolKindVariable {
|
||||
nameToExpandoTargetIndex.Add(symbol.Name, i)
|
||||
}
|
||||
if symbol.Kind == lsproto.SymbolKindNamespace {
|
||||
if _, ok := nameToNamespaceIndex[symbol.Name]; !ok {
|
||||
nameToNamespaceIndex[symbol.Name] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, symbol := range symbols {
|
||||
if symbol.Children != nil {
|
||||
children := mergeExpandos(*symbol.Children)
|
||||
symbol.Children = &children
|
||||
}
|
||||
|
||||
// Anonymous symbols never merge.
|
||||
if isAnonymousName(symbol.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Merge expandos.
|
||||
if symbol.Kind == lsproto.SymbolKindProperty {
|
||||
symbolsWithSameName := nameToExpandoTargetIndex.Get(symbol.Name)
|
||||
for j := len(symbolsWithSameName) - 1; j >= 0; j-- {
|
||||
targetIndex := symbolsWithSameName[j]
|
||||
targetSymbol := symbols[targetIndex]
|
||||
mergeChildren(targetSymbol, symbol)
|
||||
// Mark this symbol as merged.
|
||||
symbols[i] = nil
|
||||
}
|
||||
}
|
||||
// Merge namespaces.
|
||||
if symbol.Kind == lsproto.SymbolKindNamespace {
|
||||
if targetIndex, ok := nameToNamespaceIndex[symbol.Name]; ok && targetIndex != i {
|
||||
targetSymbol := symbols[targetIndex]
|
||||
mergeChildren(targetSymbol, symbol)
|
||||
// Mark this symbol as merged.
|
||||
symbols[i] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, symbol := range symbols {
|
||||
if symbol != nil {
|
||||
mergedSymbols = append(mergedSymbols, symbol)
|
||||
}
|
||||
}
|
||||
return mergedSymbols
|
||||
}
|
||||
|
||||
func mergeChildren(target *lsproto.DocumentSymbol, source *lsproto.DocumentSymbol) {
|
||||
if source.Children != nil {
|
||||
if target.Children == nil {
|
||||
target.Children = source.Children
|
||||
} else {
|
||||
*target.Children = mergeExpandos(append(*target.Children, *source.Children...))
|
||||
slices.SortFunc(*target.Children, func(a, b *lsproto.DocumentSymbol) int {
|
||||
return lsproto.CompareRanges(a.Range, b.Range)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See `getUnnamedNodeLabel`.
|
||||
func isAnonymousName(name string) bool {
|
||||
return name == "<function>" || name == "<class>" || name == "export=" || name == "default" ||
|
||||
name == "constructor" || name == "()" || name == "new()" || name == "[]" || strings.HasSuffix(name, ") callback")
|
||||
}
|
||||
|
||||
func getTextOfName(node *ast.Node) string {
|
||||
switch node.Kind {
|
||||
case ast.KindIdentifier, ast.KindPrivateIdentifier, ast.KindNumericLiteral:
|
||||
return node.Text()
|
||||
case ast.KindStringLiteral:
|
||||
return "\"" + printer.EscapeString(node.Text(), '"') + "\""
|
||||
case ast.KindNoSubstitutionTemplateLiteral:
|
||||
return "`" + printer.EscapeString(node.Text(), '`') + "`"
|
||||
case ast.KindComputedPropertyName:
|
||||
if ast.IsStringOrNumericLiteralLike(node.Expression()) {
|
||||
return getTextOfName(node.Expression())
|
||||
}
|
||||
}
|
||||
return scanner.GetTextOfNode(node)
|
||||
}
|
||||
|
||||
func getUnnamedNodeLabel(node *ast.Node) string {
|
||||
if parent := ast.WalkUpParenthesizedExpressions(node.Parent); parent != nil && ast.IsExportAssignment(parent) {
|
||||
if parent.AsExportAssignment().IsExportEquals {
|
||||
return "export="
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
switch node.Kind {
|
||||
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction:
|
||||
if node.ModifierFlags()&ast.ModifierFlagsDefault != 0 {
|
||||
return "default"
|
||||
}
|
||||
if ast.IsCallExpression(node.Parent) {
|
||||
name := getCallExpressionName(node.Parent.Expression())
|
||||
if name != "" {
|
||||
name = cleanCallbackText(name)
|
||||
if len(name) > maxLength {
|
||||
return name + " callback"
|
||||
}
|
||||
args := cleanCallbackText(getCallExpressionLiteralArgs(node.Parent))
|
||||
return name + "(" + args + ") callback"
|
||||
}
|
||||
}
|
||||
return "<function>"
|
||||
case ast.KindClassDeclaration, ast.KindClassExpression:
|
||||
if node.ModifierFlags()&ast.ModifierFlagsDefault != 0 {
|
||||
return "default"
|
||||
}
|
||||
return "<class>"
|
||||
case ast.KindConstructor:
|
||||
return "constructor"
|
||||
case ast.KindCallSignature:
|
||||
return "()"
|
||||
case ast.KindConstructSignature:
|
||||
return "new()"
|
||||
case ast.KindIndexSignature:
|
||||
return "[]"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getCallExpressionName(node *ast.Node) string {
|
||||
switch node.Kind {
|
||||
case ast.KindIdentifier, ast.KindPrivateIdentifier:
|
||||
return node.Text()
|
||||
case ast.KindPropertyAccessExpression:
|
||||
left := getCallExpressionName(node.Expression())
|
||||
right := getCallExpressionName(node.Name())
|
||||
if left != "" {
|
||||
return left + "." + right
|
||||
}
|
||||
return right
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getCallExpressionLiteralArgs(callExpr *ast.Node) string {
|
||||
var parts []string
|
||||
for _, arg := range callExpr.Arguments() {
|
||||
if ast.IsStringLiteralLike(arg) || ast.IsTemplateExpression(arg) {
|
||||
parts = append(parts, scanner.GetTextOfNode(arg))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
func cleanCallbackText(text string) string {
|
||||
truncated := stringutil.TruncateByRunes(text, maxLength)
|
||||
if len(truncated) < len(text) {
|
||||
text = truncated + "..."
|
||||
}
|
||||
return strings.Map(func(r rune) rune {
|
||||
if stringutil.IsLineBreak(r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, text)
|
||||
}
|
||||
|
||||
func getInteriorModule(node *ast.Node) *ast.Node {
|
||||
for node.Body() != nil && ast.IsModuleDeclaration(node.Body()) {
|
||||
node = node.Body()
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func getModuleName(node *ast.Node) string {
|
||||
result := node.Name().Text()
|
||||
for node.Body() != nil && ast.IsModuleDeclaration(node.Body()) {
|
||||
node = node.Body()
|
||||
result = result + "." + node.Name().Text()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type DeclarationInfo struct {
|
||||
name string
|
||||
declaration *ast.Node
|
||||
matchScore int
|
||||
}
|
||||
|
||||
func ProvideWorkspaceSymbols(
|
||||
ctx context.Context,
|
||||
programs []*compiler.Program,
|
||||
converters *lsconv.Converters,
|
||||
preferences lsutil.UserPreferences,
|
||||
query string,
|
||||
) (lsproto.WorkspaceSymbolResponse, error) {
|
||||
excludeLibrarySymbols := preferences.ExcludeLibrarySymbolsInNavTo.IsTrue()
|
||||
// Obtain set of non-declaration source files from all active programs.
|
||||
sourceFiles := map[tspath.Path]*ast.SourceFile{}
|
||||
for _, program := range programs {
|
||||
for _, sourceFile := range program.SourceFiles() {
|
||||
if (program.HasTSFile() || !sourceFile.IsDeclarationFile) &&
|
||||
!shouldExcludeFile(sourceFile, program, excludeLibrarySymbols) {
|
||||
sourceFiles[sourceFile.Path()] = sourceFile
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create DeclarationInfos for all declarations in the source files.
|
||||
var infos []DeclarationInfo
|
||||
for _, sourceFile := range sourceFiles {
|
||||
if ctx.Err() != nil {
|
||||
return lsproto.SymbolInformationsOrWorkspaceSymbolsOrNull{}, nil
|
||||
}
|
||||
declarationMap := sourceFile.GetDeclarationMap()
|
||||
for name, declarations := range declarationMap {
|
||||
score := getMatchScore(name, query)
|
||||
if score >= 0 {
|
||||
for _, declaration := range declarations {
|
||||
infos = append(infos, DeclarationInfo{name, declaration, score})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort the DeclarationInfos and return the top 256 matches.
|
||||
slices.SortFunc(infos, compareDeclarationInfos)
|
||||
count := min(len(infos), 256)
|
||||
symbols := make([]*lsproto.SymbolInformation, count)
|
||||
for i, info := range infos[0:count] {
|
||||
node := info.declaration
|
||||
sourceFile := ast.GetSourceFileOfNode(node)
|
||||
container := getContainerNode(info.declaration)
|
||||
var containerName *string
|
||||
if container != nil {
|
||||
containerName = strPtrTo(ast.GetDeclarationName(container))
|
||||
}
|
||||
// Use the name node's span so that VS selects just the symbol name (matching
|
||||
// the TS5 navto behaviour). GetNameOfDeclaration is always non-nil here because
|
||||
// computeDeclarationMap only adds declarations whose GetDeclarationName (string
|
||||
// form) is non-empty, which implies a name node exists.
|
||||
nameNode := ast.GetNameOfDeclaration(node)
|
||||
nameStart := astnav.GetStartOfNode(nameNode, sourceFile, false /*includeJsDoc*/)
|
||||
nameRange := core.NewTextRange(nameStart, nameNode.End())
|
||||
var symbol lsproto.SymbolInformation
|
||||
symbol.Name = info.name
|
||||
symbol.Kind = getSymbolKindFromNode(info.declaration)
|
||||
symbol.Location = converters.ToLSPLocation(sourceFile, nameRange)
|
||||
symbol.ContainerName = containerName
|
||||
symbols[i] = &symbol
|
||||
}
|
||||
|
||||
return lsproto.SymbolInformationsOrWorkspaceSymbolsOrNull{SymbolInformations: &symbols}, nil
|
||||
}
|
||||
|
||||
func shouldExcludeFile(file *ast.SourceFile, program *compiler.Program, excludeLibrarySymbols bool) bool {
|
||||
return excludeLibrarySymbols && (isInsideNodeModules(file.FileName()) || program.IsLibFile(file))
|
||||
}
|
||||
|
||||
func isInsideNodeModules(fileName string) bool {
|
||||
return strings.Contains(fileName, "/node_modules/")
|
||||
}
|
||||
|
||||
// Return a score for matching `s` against `pattern`. In order to match, `s` must contain each of the characters in
|
||||
// `pattern` in the same order. Upper case characters in `pattern` must match exactly, whereas lower case characters
|
||||
// in `pattern` match either case in `s`. If `s` doesn't match, -1 is returned. Otherwise, the returned score is the
|
||||
// number of characters in `s` that weren't matched. Thus, zero represents an exact match, and higher values represent
|
||||
// increasingly less specific partial matches.
|
||||
func getMatchScore(s string, pattern string) int {
|
||||
score := 0
|
||||
for _, p := range pattern {
|
||||
exact := unicode.IsUpper(p)
|
||||
for {
|
||||
c, size := utf8.DecodeRuneInString(s)
|
||||
if size == 0 {
|
||||
return -1
|
||||
}
|
||||
s = s[size:]
|
||||
if exact && c == p || !exact && unicode.ToLower(c) == unicode.ToLower(p) {
|
||||
break
|
||||
}
|
||||
score++
|
||||
}
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// Sort DeclarationInfos by ascending match score, then ascending case insensitive name, then
|
||||
// ascending case sensitive name, and finally by source file name and position.
|
||||
func compareDeclarationInfos(d1, d2 DeclarationInfo) int {
|
||||
if d1.matchScore != d2.matchScore {
|
||||
return d1.matchScore - d2.matchScore
|
||||
}
|
||||
if c := stringutil.CompareStringsCaseInsensitive(d1.name, d2.name); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := strings.Compare(d1.name, d2.name); c != 0 {
|
||||
return c
|
||||
}
|
||||
s1 := ast.GetSourceFileOfNode(d1.declaration)
|
||||
s2 := ast.GetSourceFileOfNode(d2.declaration)
|
||||
if s1 != s2 {
|
||||
return strings.Compare(string(s1.Path()), string(s2.Path()))
|
||||
}
|
||||
return d1.declaration.Pos() - d2.declaration.Pos()
|
||||
}
|
||||
|
||||
// getSymbolKindFromNode converts an AST node to an LSP SymbolKind.
|
||||
// Combines getNodeKind with VS Code's fromProtocolScriptElementKind.
|
||||
func getSymbolKindFromNode(node *ast.Node) lsproto.SymbolKind {
|
||||
switch node.Kind {
|
||||
case ast.KindSourceFile:
|
||||
if ast.IsExternalModule(node.AsSourceFile()) {
|
||||
return lsproto.SymbolKindModule
|
||||
}
|
||||
return lsproto.SymbolKindFile
|
||||
case ast.KindModuleDeclaration:
|
||||
return lsproto.SymbolKindNamespace
|
||||
case ast.KindClassDeclaration, ast.KindClassExpression:
|
||||
return lsproto.SymbolKindClass
|
||||
case ast.KindInterfaceDeclaration:
|
||||
return lsproto.SymbolKindInterface
|
||||
case ast.KindTypeAliasDeclaration, ast.KindJSDocTypedefTag, ast.KindJSDocCallbackTag:
|
||||
return lsproto.SymbolKindClass
|
||||
case ast.KindEnumDeclaration:
|
||||
return lsproto.SymbolKindEnum
|
||||
case ast.KindVariableDeclaration:
|
||||
return lsproto.SymbolKindVariable
|
||||
case ast.KindArrowFunction, ast.KindFunctionDeclaration, ast.KindFunctionExpression:
|
||||
return lsproto.SymbolKindFunction
|
||||
case ast.KindGetAccessor, ast.KindSetAccessor:
|
||||
return lsproto.SymbolKindProperty
|
||||
case ast.KindMethodDeclaration, ast.KindMethodSignature:
|
||||
return lsproto.SymbolKindMethod
|
||||
case ast.KindPropertyDeclaration, ast.KindPropertySignature, ast.KindPropertyAssignment,
|
||||
ast.KindShorthandPropertyAssignment, ast.KindSpreadAssignment, ast.KindIndexSignature:
|
||||
return lsproto.SymbolKindProperty
|
||||
case ast.KindCallSignature:
|
||||
return lsproto.SymbolKindMethod
|
||||
case ast.KindConstructSignature:
|
||||
return lsproto.SymbolKindConstructor
|
||||
case ast.KindConstructor, ast.KindClassStaticBlockDeclaration:
|
||||
return lsproto.SymbolKindConstructor
|
||||
case ast.KindTypeParameter:
|
||||
return lsproto.SymbolKindTypeParameter
|
||||
case ast.KindEnumMember:
|
||||
return lsproto.SymbolKindEnumMember
|
||||
case ast.KindParameter:
|
||||
if ast.HasSyntacticModifier(node, ast.ModifierFlagsParameterPropertyModifier) {
|
||||
return lsproto.SymbolKindProperty
|
||||
}
|
||||
return lsproto.SymbolKindVariable
|
||||
case ast.KindBinaryExpression, ast.KindCallExpression:
|
||||
kind := ast.GetAssignmentDeclarationKind(node)
|
||||
switch kind {
|
||||
case ast.JSDeclarationKindThisProperty, ast.JSDeclarationKindProperty, ast.JSDeclarationKindObjectDefinePropertyValue:
|
||||
return lsproto.SymbolKindProperty
|
||||
}
|
||||
case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral, ast.KindNumericLiteral:
|
||||
// String literals used as property names (e.g., in Object.defineProperty)
|
||||
return lsproto.SymbolKindProperty
|
||||
}
|
||||
return lsproto.SymbolKindVariable
|
||||
}
|
||||
1404
tools/tsgo/internal/ls/utilities.go
Normal file
1404
tools/tsgo/internal/ls/utilities.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user