remove unused packages

This commit is contained in:
Egor Aristov 2025-10-15 17:27:47 +03:00
parent 5742ff6b75
commit 0cea2e734b
Signed by: egor3f
GPG Key ID: 40482A264AAEC85F
32 changed files with 0 additions and 19075 deletions

View File

@ -1,44 +0,0 @@
package ls
import (
"context"
"errors"
"fmt"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/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)
}

View File

@ -1,27 +0,0 @@
// Code generated by "stringer -type=ExportKind -output=autoImports_stringer_generated.go"; DO NOT EDIT.
package ls
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[ExportKindNamed-0]
_ = x[ExportKindDefault-1]
_ = x[ExportKindExportEquals-2]
_ = x[ExportKindUMD-3]
_ = x[ExportKindModule-4]
}
const _ExportKind_name = "ExportKindNamedExportKindDefaultExportKindExportEqualsExportKindUMDExportKindModule"
var _ExportKind_index = [...]uint8{0, 15, 32, 54, 67, 83}
func (i ExportKind) String() string {
if i < 0 || i >= ExportKind(len(_ExportKind_index)-1) {
return "ExportKind(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ExportKind_name[_ExportKind_index[i]:_ExportKind_index[i+1]]
}

View File

@ -1,325 +0,0 @@
package ls
import (
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug"
)
type Import struct {
name string
kind ImportKind // ImportKindCommonJS | ImportKindNamespace
addAsTypeOnly AddAsTypeOnly
propertyName string // Use when needing to generate an `ImportSpecifier with a `propertyName`; the name preceding "as" keyword (propertyName = "" when "as" is absent)
}
func (ct *changeTracker) addNamespaceQualifier(sourceFile *ast.SourceFile, qualification *Qualification) {
ct.insertText(sourceFile, qualification.usagePosition, qualification.namespacePrefix+".")
}
func (ct *changeTracker) doAddExistingFix(
sourceFile *ast.SourceFile,
clause *ast.Node, // ImportClause | ObjectBindingPattern,
defaultImport *Import,
namedImports []*Import,
// removeExistingImportSpecifiers *core.Set[ImportSpecifier | BindingElement] // !!! remove imports not implemented
preferences *UserPreferences,
) {
switch clause.Kind {
case ast.KindObjectBindingPattern:
if clause.Kind == ast.KindObjectBindingPattern {
// bindingPattern := clause.AsBindingPattern()
// !!! adding *and* removing imports not implemented
// if (removeExistingImportSpecifiers && core.Some(bindingPattern.Elements, func(e *ast.Node) bool {
// return removeExistingImportSpecifiers.Has(e)
// })) {
// If we're both adding and removing elements, just replace and reprint the whole
// node. The change tracker doesn't understand all the operations and can insert or
// leave behind stray commas.
// ct.replaceNode(
// sourceFile,
// bindingPattern,
// ct.NodeFactory.NewObjectBindingPattern([
// ...bindingPattern.Elements.Filter(func(e *ast.Node) bool {
// return !removeExistingImportSpecifiers.Has(e)
// }),
// ...defaultImport ? [ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, /*propertyName*/ "default", defaultImport.name)] : emptyArray,
// ...namedImports.map(i => ct.NodeFactory.createBindingElement(/*dotDotDotToken*/ nil, i.propertyName, i.name)),
// ]),
// )
// return
// }
if defaultImport != nil {
ct.addElementToBindingPattern(sourceFile, clause, defaultImport.name, ptrTo("default"))
}
for _, specifier := range namedImports {
ct.addElementToBindingPattern(sourceFile, clause, specifier.name, &specifier.propertyName)
}
return
}
case ast.KindImportClause:
importClause := clause.AsImportClause()
// promoteFromTypeOnly = true if we need to promote the entire original clause from type only
promoteFromTypeOnly := importClause.IsTypeOnly() && core.Some(append(namedImports, defaultImport), func(i *Import) bool {
if i == nil {
return false
}
return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed
})
existingSpecifiers := []*ast.Node{} // []*ast.ImportSpecifier
if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamedImports {
existingSpecifiers = importClause.NamedBindings.Elements()
}
if defaultImport != nil {
debug.Assert(clause.Name() == nil, "Cannot add a default import to an import clause that already has one")
ct.insertNodeAt(sourceFile, core.TextPos(astnav.GetStartOfNode(clause, sourceFile, false)), ct.NodeFactory.NewIdentifier(defaultImport.name), changeNodeOptions{suffix: ", "})
}
if len(namedImports) > 0 {
// !!! OrganizeImports not yet implemented
// specifierComparer, isSorted := OrganizeImports.getNamedImportSpecifierComparerWithDetection(importClause.Parent, preferences, sourceFile);
newSpecifiers := core.Map(namedImports, func(namedImport *Import) *ast.Node {
var identifier *ast.Node
if namedImport.propertyName != "" {
identifier = ct.NodeFactory.NewIdentifier(namedImport.propertyName).AsIdentifier().AsNode()
}
return ct.NodeFactory.NewImportSpecifier(
(!importClause.IsTypeOnly() || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences),
identifier,
ct.NodeFactory.NewIdentifier(namedImport.name),
)
}) // !!! sort with specifierComparer
// !!! remove imports not implemented
// if (removeExistingImportSpecifiers) {
// // If we're both adding and removing specifiers, just replace and reprint the whole
// // node. The change tracker doesn't understand all the operations and can insert or
// // leave behind stray commas.
// ct.replaceNode(
// sourceFile,
// importClause.NamedBindings,
// ct.NodeFactory.updateNamedImports(
// importClause.NamedBindings.AsNamedImports(),
// append(core.Filter(existingSpecifiers, func (s *ast.ImportSpecifier) bool {return !removeExistingImportSpecifiers.Has(s)}), newSpecifiers...), // !!! sort with specifierComparer
// ),
// );
// } else if (len(existingSpecifiers) > 0 && isSorted != false) {
// !!! OrganizeImports not implemented
// The sorting preference computed earlier may or may not have validated that these particular
// import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return
// nonsense. So if there are existing specifiers, even if we know the sorting preference, we
// need to ensure that the existing specifiers are sorted according to the preference in order
// to do a sorted insertion.
// changed to check if existing specifiers are sorted
// if we're promoting the clause from type-only, we need to transform the existing imports before attempting to insert the new named imports
// transformedExistingSpecifiers := existingSpecifiers
// if promoteFromTypeOnly && existingSpecifiers {
// transformedExistingSpecifiers = ct.NodeFactory.updateNamedImports(
// importClause.NamedBindings.AsNamedImports(),
// core.SameMap(existingSpecifiers, func(e *ast.ImportSpecifier) *ast.ImportSpecifier {
// return ct.NodeFactory.updateImportSpecifier(e, /*isTypeOnly*/ true, e.propertyName, e.name)
// }),
// ).elements
// }
// for _, spec := range newSpecifiers {
// insertionIndex := OrganizeImports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec, specifierComparer);
// ct.insertImportSpecifierAtIndex(sourceFile, spec, importClause.namedBindings as NamedImports, insertionIndex);
// }
// } else
if len(existingSpecifiers) > 0 {
for _, spec := range newSpecifiers {
ct.insertNodeInListAfter(sourceFile, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), existingSpecifiers)
}
} else {
if len(newSpecifiers) > 0 {
namedImports := ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(newSpecifiers))
if importClause.NamedBindings != nil {
ct.replaceNode(sourceFile, importClause.NamedBindings, namedImports, nil)
} else {
if clause.Name() == nil {
panic("Import clause must have either named imports or a default import")
}
ct.insertNodeAfter(sourceFile, clause.Name(), namedImports)
}
}
}
}
if promoteFromTypeOnly {
// !!! promote type-only imports not implemented
// ct.delete(sourceFile, getTypeKeywordOfTypeOnlyImport(clause, sourceFile));
// if (existingSpecifiers) {
// // We used to convert existing specifiers to type-only only if compiler options indicated that
// // would be meaningful (see the `importNameElisionDisabled` utility function), but user
// // feedback indicated a preference for preserving the type-onlyness of existing specifiers
// // regardless of whether it would make a difference in emit.
// for _, specifier := range existingSpecifiers {
// ct.insertModifierBefore(sourceFile, SyntaxKind.TypeKeyword, specifier);
// }
// }
}
default:
panic("Unsupported clause kind: " + clause.Kind.String() + "for doAddExistingFix")
}
}
func (ct *changeTracker) addElementToBindingPattern(sourceFile *ast.SourceFile, bindingPattern *ast.Node, name string, propertyName *string) {
element := ct.newBindingElementFromNameAndPropertyName(name, propertyName)
if len(bindingPattern.Elements()) > 0 {
ct.insertNodeInListAfter(sourceFile, bindingPattern.Elements()[len(bindingPattern.Elements())-1], element, nil)
} else {
ct.replaceNode(sourceFile, bindingPattern, ct.NodeFactory.NewBindingPattern(
ast.KindObjectBindingPattern,
ct.NodeFactory.NewNodeList([]*ast.Node{element}),
), nil)
}
}
func (ct *changeTracker) newBindingElementFromNameAndPropertyName(name string, propertyName *string) *ast.Node {
var newPropertyNameIdentifier *ast.Node
if propertyName != nil {
newPropertyNameIdentifier = ct.NodeFactory.NewIdentifier(*propertyName)
}
return ct.NodeFactory.NewBindingElement(
nil, /*dotDotDotToken*/
newPropertyNameIdentifier,
ct.NodeFactory.NewIdentifier(name),
nil, /* initializer */
)
}
func (ct *changeTracker) insertImports(sourceFile *ast.SourceFile, imports []*ast.Statement, blankLineBetween bool, preferences *UserPreferences) {
var existingImportStatements []*ast.Statement
if imports[0].Kind == ast.KindVariableStatement {
existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsRequireVariableStatement)
} else {
existingImportStatements = core.Filter(sourceFile.Statements.Nodes, ast.IsAnyImportSyntax)
}
// !!! OrganizeImports
// { comparer, isSorted } := OrganizeImports.getOrganizeImportsStringComparerWithDetection(existingImportStatements, preferences);
// sortedNewImports := isArray(imports) ? toSorted(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports];
sortedNewImports := imports
// !!! FutureSourceFile
// if !isFullSourceFile(sourceFile) {
// for _, newImport := range sortedNewImports {
// // Insert one at a time to send correct original source file for accurate text reuse
// // when some imports are cloned from existing ones in other files.
// ct.insertStatementsInNewFile(sourceFile.fileName, []*ast.Node{newImport}, ast.GetSourceFileOfNode(getOriginalNode(newImport)))
// }
// return;
// }
// if len(existingImportStatements) > 0 && isSorted {
// for _, newImport := range sortedNewImports {
// insertionIndex := OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer)
// if insertionIndex == 0 {
// // If the first import is top-of-file, insert after the leading comment which is likely the header.
// options := existingImportStatements[0] == sourceFile.statements[0] ? { leadingTriviaOption: textchanges.LeadingTriviaOption.Exclude } : {};
// ct.insertNodeBefore(sourceFile, existingImportStatements[0], newImport, /*blankLineBetween*/ false, options);
// } else {
// prevImport := existingImportStatements[insertionIndex - 1]
// ct.insertNodeAfter(sourceFile, prevImport, newImport);
// }
// }
// return
// }
if len(existingImportStatements) > 0 {
ct.insertNodesAfter(sourceFile, existingImportStatements[len(existingImportStatements)-1], sortedNewImports)
} else {
ct.insertAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween)
}
}
func (ct *changeTracker) makeImport(defaultImport *ast.IdentifierNode, namedImports []*ast.Node, moduleSpecifier *ast.Expression, isTypeOnly bool) *ast.Statement {
var newNamedImports *ast.Node
if len(namedImports) > 0 {
newNamedImports = ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(namedImports))
}
var importClause *ast.Node
if defaultImport != nil || newNamedImports != nil {
importClause = ct.NodeFactory.NewImportClause(core.IfElse(isTypeOnly, ast.KindTypeKeyword, ast.KindUnknown), defaultImport, newNamedImports)
}
return ct.NodeFactory.NewImportDeclaration( /*modifiers*/ nil, importClause, moduleSpecifier, nil /*attributes*/)
}
func (ct *changeTracker) getNewImports(
moduleSpecifier string,
// quotePreference quotePreference, // !!! quotePreference
defaultImport *Import,
namedImports []*Import,
namespaceLikeImport *Import, // { importKind: ImportKind.CommonJS | ImportKind.Namespace; }
compilerOptions *core.CompilerOptions,
preferences *UserPreferences,
) []*ast.Statement {
moduleSpecifierStringLiteral := ct.NodeFactory.NewStringLiteral(moduleSpecifier)
var statements []*ast.Statement // []AnyImportSyntax
if defaultImport != nil || len(namedImports) > 0 {
// `verbatimModuleSyntax` should prefer top-level `import type` -
// even though it's not an error, it would add unnecessary runtime emit.
topLevelTypeOnly := (defaultImport == nil || needsTypeOnly(defaultImport.addAsTypeOnly)) &&
core.Every(namedImports, func(i *Import) bool { return needsTypeOnly(i.addAsTypeOnly) }) ||
(compilerOptions.VerbatimModuleSyntax.IsTrue() || preferences.PreferTypeOnlyAutoImports) &&
defaultImport != nil && defaultImport.addAsTypeOnly != AddAsTypeOnlyNotAllowed && !core.Some(namedImports, func(i *Import) bool { return i.addAsTypeOnly == AddAsTypeOnlyNotAllowed })
var defaultImportNode *ast.Node
if defaultImport != nil {
defaultImportNode = ct.NodeFactory.NewIdentifier(defaultImport.name)
}
statements = append(statements, ct.makeImport(defaultImportNode, core.Map(namedImports, func(namedImport *Import) *ast.Node {
var namedImportPropertyName *ast.Node
if namedImport.propertyName != "" {
namedImportPropertyName = ct.NodeFactory.NewIdentifier(namedImport.propertyName)
}
return ct.NodeFactory.NewImportSpecifier(
!topLevelTypeOnly && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences),
namedImportPropertyName,
ct.NodeFactory.NewIdentifier(namedImport.name),
)
}), moduleSpecifierStringLiteral, topLevelTypeOnly))
}
if namespaceLikeImport != nil {
var declaration *ast.Statement
if namespaceLikeImport.kind == ImportKindCommonJS {
declaration = ct.NodeFactory.NewImportEqualsDeclaration(
/*modifiers*/ nil,
shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences),
ct.NodeFactory.NewIdentifier(namespaceLikeImport.name),
ct.NodeFactory.NewExternalModuleReference(moduleSpecifierStringLiteral),
)
} else {
declaration = ct.NodeFactory.NewImportDeclaration(
/*modifiers*/ nil,
ct.NodeFactory.NewImportClause(
/*phaseModifier*/ core.IfElse(shouldUseTypeOnly(namespaceLikeImport.addAsTypeOnly, preferences), ast.KindTypeKeyword, ast.KindUnknown),
/*name*/ nil,
ct.NodeFactory.NewNamespaceImport(ct.NodeFactory.NewIdentifier(namespaceLikeImport.name)),
),
moduleSpecifierStringLiteral,
/*attributes*/ nil,
)
}
statements = append(statements, declaration)
}
if len(statements) == 0 {
panic("No statements to insert for new imports")
}
return statements
}
func needsTypeOnly(addAsTypeOnly AddAsTypeOnly) bool {
return addAsTypeOnly == AddAsTypeOnlyRequired
}
func shouldUseTypeOnly(addAsTypeOnly AddAsTypeOnly, preferences *UserPreferences) bool {
return needsTypeOnly(addAsTypeOnly) || addAsTypeOnly != AddAsTypeOnlyNotAllowed && preferences.PreferTypeOnlyAutoImports
}

File diff suppressed because it is too large Load Diff

View File

@ -1,181 +0,0 @@
package ls
import (
"context"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
)
func (l *LanguageService) getExportInfos(
ctx context.Context,
ch *checker.Checker,
importingFile *ast.SourceFile,
preferences *UserPreferences,
exportMapKey ExportInfoMapKey,
) []*SymbolExportInfo {
expInfoMap := NewExportInfoMap(l.GetProgram().GetGlobalTypingsCacheLocation())
moduleCount := 0
symbolNameMatch := func(symbolName string) bool {
return symbolName == exportMapKey.SymbolName
}
forEachExternalModuleToImportFrom(
ch,
l.GetProgram(),
preferences,
// /*useAutoImportProvider*/ true,
func(moduleSymbol *ast.Symbol, moduleFile *ast.SourceFile, ch *checker.Checker, isFromPackageJson bool) {
if moduleCount = moduleCount + 1; moduleCount%100 == 0 && ctx.Err() != nil {
return
}
if moduleFile == nil && moduleSymbol.Name != exportMapKey.AmbientModuleName {
return
}
seenExports := collections.Set[string]{}
defaultInfo := getDefaultLikeExportInfo(moduleSymbol, ch)
var exportingModuleSymbol *ast.Symbol
if defaultInfo != nil {
exportingModuleSymbol = defaultInfo.exportingModuleSymbol
// Note: I think we shouldn't actually see resolved module symbols here, but weird merges
// can cause it to happen: see 'completionsImport_mergedReExport.ts'
if isImportableSymbol(exportingModuleSymbol, ch) {
expInfoMap.add(
importingFile.Path(),
exportingModuleSymbol,
core.IfElse(defaultInfo.exportKind == ExportKindDefault, ast.InternalSymbolNameDefault, ast.InternalSymbolNameExportEquals),
moduleSymbol,
moduleFile,
defaultInfo.exportKind,
isFromPackageJson,
ch,
symbolNameMatch,
nil,
)
}
}
ch.ForEachExportAndPropertyOfModule(moduleSymbol, func(exported *ast.Symbol, key string) {
if exported != exportingModuleSymbol && isImportableSymbol(exported, ch) && seenExports.AddIfAbsent(key) {
expInfoMap.add(
importingFile.Path(),
exported,
key,
moduleSymbol,
moduleFile,
ExportKindNamed,
isFromPackageJson,
ch,
symbolNameMatch,
nil,
)
}
})
})
return expInfoMap.get(importingFile.Path(), ch, exportMapKey)
}
func (l *LanguageService) searchExportInfosForCompletions(
ctx context.Context,
ch *checker.Checker,
importingFile *ast.SourceFile,
preferences *UserPreferences,
isForImportStatementCompletion bool,
isRightOfOpenTag bool,
isTypeOnlyLocation bool,
lowerCaseTokenText string,
action func([]*SymbolExportInfo, string, bool, ExportInfoMapKey) []*SymbolExportInfo,
) {
symbolNameMatches := map[string]bool{}
symbolNameMatch := func(symbolName string) bool {
if !scanner.IsIdentifierText(symbolName, importingFile.LanguageVariant) {
return false
}
if b, ok := symbolNameMatches[symbolName]; ok {
return b
}
if isNonContextualKeyword(scanner.StringToToken(symbolName)) {
symbolNameMatches[symbolName] = false
return false
}
// Do not try to auto-import something with a lowercase first letter for a JSX tag
firstChar := rune(symbolName[0])
if isRightOfOpenTag && (firstChar < 'A' || firstChar > 'Z') {
symbolNameMatches[symbolName] = false
return false
}
symbolNameMatches[symbolName] = charactersFuzzyMatchInString(symbolName, lowerCaseTokenText)
return symbolNameMatches[symbolName]
}
flagMatch := func(targetFlags ast.SymbolFlags) bool {
if !isTypeOnlyLocation && !isForImportStatementCompletion && (targetFlags&ast.SymbolFlagsValue) == 0 {
return false
}
if isTypeOnlyLocation && (targetFlags&(ast.SymbolFlagsModule|ast.SymbolFlagsType) == 0) {
return false
}
return true
}
expInfoMap := NewExportInfoMap(l.GetProgram().GetGlobalTypingsCacheLocation())
moduleCount := 0
forEachExternalModuleToImportFrom(
ch,
l.GetProgram(),
preferences,
// /*useAutoImportProvider*/ true,
func(moduleSymbol *ast.Symbol, moduleFile *ast.SourceFile, ch *checker.Checker, isFromPackageJson bool) {
if moduleCount = moduleCount + 1; moduleCount%100 == 0 && ctx.Err() != nil {
return
}
seenExports := collections.Set[string]{}
defaultInfo := getDefaultLikeExportInfo(moduleSymbol, ch)
// Note: I think we shouldn't actually see resolved module symbols here, but weird merges
// can cause it to happen: see 'completionsImport_mergedReExport.ts'
if defaultInfo != nil && isImportableSymbol(defaultInfo.exportingModuleSymbol, ch) {
expInfoMap.add(
importingFile.Path(),
defaultInfo.exportingModuleSymbol,
core.IfElse(defaultInfo.exportKind == ExportKindDefault, ast.InternalSymbolNameDefault, ast.InternalSymbolNameExportEquals),
moduleSymbol,
moduleFile,
defaultInfo.exportKind,
isFromPackageJson,
ch,
symbolNameMatch,
flagMatch,
)
}
var exportingModuleSymbol *ast.Symbol
if defaultInfo != nil {
exportingModuleSymbol = defaultInfo.exportingModuleSymbol
}
ch.ForEachExportAndPropertyOfModule(moduleSymbol, func(exported *ast.Symbol, key string) {
if exported != exportingModuleSymbol && isImportableSymbol(exported, ch) && seenExports.AddIfAbsent(key) {
expInfoMap.add(
importingFile.Path(),
exported,
key,
moduleSymbol,
moduleFile,
ExportKindNamed,
isFromPackageJson,
ch,
symbolNameMatch,
flagMatch,
)
}
})
})
expInfoMap.search(
ch,
importingFile.Path(),
/*preferCapitalized*/ isRightOfOpenTag,
func(symbolName string, targetFlags ast.SymbolFlags) bool {
return symbolNameMatch(symbolName) && flagMatch(targetFlags)
},
action,
)
}

View File

@ -1,215 +0,0 @@
package ls
import (
"fmt"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/modulespecifiers"
)
//go:generate go tool golang.org/x/tools/cmd/stringer -type=ExportKind -output=autoImports_stringer_generated.go
//go:generate go tool mvdan.cc/gofumpt -w autoImports_stringer_generated.go
type ImportKind int
const (
ImportKindNamed ImportKind = 0
ImportKindDefault ImportKind = 1
ImportKindNamespace ImportKind = 2
ImportKindCommonJS ImportKind = 3
)
type ExportKind int
const (
ExportKindNamed ExportKind = 0
ExportKindDefault ExportKind = 1
ExportKindExportEquals ExportKind = 2
ExportKindUMD ExportKind = 3
ExportKindModule ExportKind = 4
)
type ImportFixKind int
const (
// Sorted with the preferred fix coming first.
ImportFixKindUseNamespace ImportFixKind = 0
ImportFixKindJsdocTypeImport ImportFixKind = 1
ImportFixKindAddToExisting ImportFixKind = 2
ImportFixKindAddNew ImportFixKind = 3
ImportFixKindPromoteTypeOnly ImportFixKind = 4
)
type AddAsTypeOnly int
const (
// These should not be combined as bitflags, but are given powers of 2 values to
// easily detect conflicts between `NotAllowed` and `Required` by giving them a unique sum.
// They're also ordered in terms of increasing priority for a fix-all scenario (see
// `reduceAddAsTypeOnlyValues`).
AddAsTypeOnlyAllowed AddAsTypeOnly = 1 << 0
AddAsTypeOnlyRequired AddAsTypeOnly = 1 << 1
AddAsTypeOnlyNotAllowed AddAsTypeOnly = 1 << 2
)
type ImportFix struct {
kind ImportFixKind
isReExport *bool
exportInfo *SymbolExportInfo // !!! | FutureSymbolExportInfo | undefined
moduleSpecifierKind modulespecifiers.ResultKind
moduleSpecifier string
usagePosition *lsproto.Position
namespacePrefix *string
importClauseOrBindingPattern *ast.Node // ImportClause | ObjectBindingPattern
importKind ImportKind // ImportKindDefault | ImportKindNamed
addAsTypeOnly AddAsTypeOnly
propertyName string // !!! not implemented
useRequire bool
typeOnlyAliasDeclaration *ast.Declaration // TypeOnlyAliasDeclaration
}
func (i *ImportFix) qualification() *Qualification {
switch i.kind {
case ImportFixKindAddNew:
if i.usagePosition == nil || strPtrIsEmpty(i.namespacePrefix) {
return nil
}
fallthrough
case ImportFixKindUseNamespace:
return &Qualification{
usagePosition: *i.usagePosition,
namespacePrefix: *i.namespacePrefix,
}
}
panic(fmt.Sprintf("no qualification with ImportFixKind %v", i.kind))
}
type Qualification struct {
usagePosition lsproto.Position
namespacePrefix string
}
func getUseNamespaceImport(
moduleSpecifier string,
moduleSpecifierKind modulespecifiers.ResultKind,
namespacePrefix string,
usagePosition lsproto.Position,
) *ImportFix {
return &ImportFix{
kind: ImportFixKindUseNamespace,
moduleSpecifierKind: moduleSpecifierKind,
moduleSpecifier: moduleSpecifier,
usagePosition: ptrTo(usagePosition),
namespacePrefix: strPtrTo(namespacePrefix),
}
}
func getAddJsdocTypeImport(
moduleSpecifier string,
moduleSpecifierKind modulespecifiers.ResultKind,
usagePosition *lsproto.Position,
exportInfo *SymbolExportInfo,
isReExport *bool,
) *ImportFix {
return &ImportFix{
kind: ImportFixKindJsdocTypeImport,
isReExport: isReExport,
exportInfo: exportInfo,
moduleSpecifierKind: moduleSpecifierKind,
moduleSpecifier: moduleSpecifier,
usagePosition: usagePosition,
}
}
func getAddToExistingImport(
importClauseOrBindingPattern *ast.Node,
importKind ImportKind,
moduleSpecifier string,
moduleSpecifierKind modulespecifiers.ResultKind,
addAsTypeOnly AddAsTypeOnly,
) *ImportFix {
return &ImportFix{
kind: ImportFixKindAddToExisting,
moduleSpecifierKind: moduleSpecifierKind,
moduleSpecifier: moduleSpecifier,
importClauseOrBindingPattern: importClauseOrBindingPattern,
importKind: importKind,
addAsTypeOnly: addAsTypeOnly,
}
}
func getNewAddNewImport(
moduleSpecifier string,
moduleSpecifierKind modulespecifiers.ResultKind,
importKind ImportKind,
useRequire bool,
addAsTypeOnly AddAsTypeOnly,
exportInfo *SymbolExportInfo, // !!! | FutureSymbolExportInfo
isReExport *bool,
qualification *Qualification,
) *ImportFix {
return &ImportFix{
kind: ImportFixKindAddNew,
isReExport: isReExport,
exportInfo: exportInfo,
moduleSpecifierKind: modulespecifiers.ResultKindNone,
moduleSpecifier: moduleSpecifier,
importKind: importKind,
addAsTypeOnly: addAsTypeOnly,
useRequire: useRequire,
}
}
func getNewPromoteTypeOnlyImport(typeOnlyAliasDeclaration *ast.Declaration) *ImportFix {
// !!! function stub
return &ImportFix{
kind: ImportFixKindPromoteTypeOnly,
// isReExport *bool
// exportInfo *SymbolExportInfo // !!! | FutureSymbolExportInfo | undefined
// moduleSpecifierKind modulespecifiers.ResultKind
// moduleSpecifier string
typeOnlyAliasDeclaration: typeOnlyAliasDeclaration,
}
}
/** Information needed to augment an existing import declaration. */
// !!! after full implementation, rename to AddToExistingImportInfo
type FixAddToExistingImportInfo struct {
declaration *ast.Declaration
importKind ImportKind
targetFlags ast.SymbolFlags
symbol *ast.Symbol
}
func (info *FixAddToExistingImportInfo) getNewImportFromExistingSpecifier(
isValidTypeOnlyUseSite bool,
useRequire bool,
ch *checker.Checker,
compilerOptions *core.CompilerOptions,
) *ImportFix {
moduleSpecifier := checker.TryGetModuleSpecifierFromDeclaration(info.declaration)
if moduleSpecifier == nil || moduleSpecifier.Text() == "" {
return nil
}
addAsTypeOnly := AddAsTypeOnlyNotAllowed
if !useRequire {
addAsTypeOnly = getAddAsTypeOnly(isValidTypeOnlyUseSite, info.symbol, info.targetFlags, ch, compilerOptions)
}
return getNewAddNewImport(
moduleSpecifier.Text(),
modulespecifiers.ResultKindNone,
info.importKind,
useRequire,
addAsTypeOnly,
nil, // exportInfo
nil, // isReExport
nil, // qualification
)
}

View File

@ -1,336 +0,0 @@
package ls
import (
"context"
"slices"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/format"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
)
type changeNodeOptions 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 changeNodeOptions
}
type changeTracker struct {
// initialized with
formatSettings *format.FormatCodeSettings
newLine string
ls *LanguageService
ctx context.Context
*printer.EmitContext
*ast.NodeFactory
changes *collections.MultiMap[*ast.SourceFile, *trackerEdit]
// created during call to getChanges
writer *printer.ChangeTrackerWriter
// printer
}
func (ls *LanguageService) newChangeTracker(ctx context.Context) *changeTracker {
emitContext := printer.NewEmitContext()
newLine := ls.GetProgram().Options().NewLine.GetNewLineCharacter()
formatCodeSettings := format.GetDefaultFormatCodeSettings(newLine) // !!! format.GetFormatCodeSettingsFromContext(ctx),
ctx = format.WithFormatCodeSettings(ctx, formatCodeSettings, newLine)
return &changeTracker{
ls: ls,
EmitContext: emitContext,
NodeFactory: &emitContext.Factory.NodeFactory,
changes: &collections.MultiMap[*ast.SourceFile, *trackerEdit]{},
ctx: ctx,
formatSettings: formatCodeSettings,
newLine: newLine,
}
}
// !!! address strada note
// - Note: after calling this, the TextChanges object must be discarded!
func (ct *changeTracker) getChanges() map[string][]*lsproto.TextEdit {
// !!! finishDeleteDeclarations
// !!! finishClassesWithNodesInsertedAtStart
changes := ct.getTextChangesFromChanges()
// !!! changes for new files
return changes
}
func (ct *changeTracker) replaceNode(sourceFile *ast.SourceFile, oldNode *ast.Node, newNode *ast.Node, options *changeNodeOptions) {
if options == nil {
// defaults to `useNonAdjustedPositions`
options = &changeNodeOptions{
leadingTriviaOption: leadingTriviaOptionExclude,
trailingTriviaOption: trailingTriviaOptionExclude,
}
}
ct.replaceRange(sourceFile, ct.getAdjustedRange(sourceFile, oldNode, oldNode, options.leadingTriviaOption, options.trailingTriviaOption), newNode, *options)
}
func (ct *changeTracker) replaceRange(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNode *ast.Node, options changeNodeOptions) {
ct.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithSingleNode, Range: lsprotoRange, options: options, Node: newNode})
}
func (ct *changeTracker) replaceRangeWithText(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, text string) {
ct.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindText, Range: lsprotoRange, NewText: text})
}
func (ct *changeTracker) replaceRangeWithNodes(sourceFile *ast.SourceFile, lsprotoRange lsproto.Range, newNodes []*ast.Node, options changeNodeOptions) {
if len(newNodes) == 1 {
ct.replaceRange(sourceFile, lsprotoRange, newNodes[0], options)
return
}
ct.changes.Add(sourceFile, &trackerEdit{kind: trackerEditKindReplaceWithMultipleNodes, Range: lsprotoRange, nodes: newNodes, options: options})
}
func (ct *changeTracker) insertText(sourceFile *ast.SourceFile, pos lsproto.Position, text string) {
ct.replaceRangeWithText(sourceFile, lsproto.Range{Start: pos, End: pos}, text)
}
func (ct *changeTracker) insertNodeAt(sourceFile *ast.SourceFile, pos core.TextPos, newNode *ast.Node, options changeNodeOptions) {
lsPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, pos)
ct.replaceRange(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNode, options)
}
func (ct *changeTracker) insertNodesAt(sourceFile *ast.SourceFile, pos core.TextPos, newNodes []*ast.Node, options changeNodeOptions) {
lsPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, pos)
ct.replaceRangeWithNodes(sourceFile, lsproto.Range{Start: lsPos, End: lsPos}, newNodes, options)
}
func (ct *changeTracker) insertNodeAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node) {
endPosition := ct.endPosForInsertNodeAfter(sourceFile, after, newNode)
ct.insertNodeAt(sourceFile, endPosition, newNode, ct.getInsertNodeAfterOptions(sourceFile, after))
}
func (ct *changeTracker) insertNodesAfter(sourceFile *ast.SourceFile, after *ast.Node, newNodes []*ast.Node) {
endPosition := ct.endPosForInsertNodeAfter(sourceFile, after, newNodes[0])
ct.insertNodesAt(sourceFile, endPosition, newNodes, ct.getInsertNodeAfterOptions(sourceFile, after))
}
func (ct *changeTracker) 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 := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End()))
ct.replaceRange(sourceFile,
lsproto.Range{Start: endPos, End: endPos},
sourceFile.GetOrCreateToken(ast.KindSemicolonToken, after.End(), after.End(), after.Parent),
changeNodeOptions{},
)
}
return core.TextPos(ct.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 (ct *changeTracker) insertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node, containingList []*ast.Node) {
if len(containingList) == 0 {
containingList = format.GetContainingList(after, sourceFile).Nodes
}
index := slices.Index(containingList, after)
if index < 0 {
return
}
if index != len(containingList)-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[index+1]
startPos := scanner.SkipTriviaEx(sourceFile.Text(), nextNode.Pos(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: false})
// write separator and leading trivia of the next element as suffix
suffix := scanner.TokenToString(nextToken.Kind) + sourceFile.Text()[nextNode.End():startPos]
ct.insertNodeAt(sourceFile, core.TextPos(startPos), newNode, changeNodeOptions{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) != 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[index-1], sourceFile, false), sourceFile)
multilineList = afterMinusOneStartLinePosition != afterStartLinePosition
}
if hasCommentsBeforeLineBreak(sourceFile.Text(), after.End()) || printer.GetLinesBetweenPositions(sourceFile, containingList[0].Pos(), containingList[len(containingList)-1].End()) != 0 {
// in this case we'll always treat containing list as multiline
multilineList = true
}
separatorString := scanner.TokenToString(separator)
end := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(after.End()))
if !multilineList {
ct.replaceRange(sourceFile, lsproto.Range{Start: end, End: end}, newNode, changeNodeOptions{prefix: separatorString})
return
}
// insert separator immediately following the 'after' node to preserve comments in trailing trivia
// !!! formatcontext
ct.replaceRange(sourceFile, lsproto.Range{Start: end, End: end}, sourceFile.GetOrCreateToken(separator, after.End(), after.End()+len(separatorString), after.Parent), changeNodeOptions{})
// use the same indentation as 'after' item
indentation := format.FindFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, ct.formatSettings)
// insert element before the line break on the line that contains 'after' element
insertPos := scanner.SkipTriviaEx(sourceFile.Text(), after.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: false})
// find position before "\n" or "\r\n"
for insertPos != after.End() && stringutil.IsLineBreak(rune(sourceFile.Text()[insertPos-1])) {
insertPos--
}
insertLSPos := ct.ls.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(insertPos))
ct.replaceRange(
sourceFile,
lsproto.Range{Start: insertLSPos, End: insertLSPos},
newNode,
changeNodeOptions{
indentation: ptrTo(indentation),
prefix: ct.newLine,
},
)
}
func (ct *changeTracker) insertAtTopOfFile(sourceFile *ast.SourceFile, insert []*ast.Statement, blankLineBetween bool) {
if len(insert) == 0 {
return
}
pos := ct.getInsertionPositionAtSourceFileTop(sourceFile)
options := changeNodeOptions{}
if pos != 0 {
options.prefix = ct.newLine
}
if !stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) {
options.suffix = ct.newLine
}
if blankLineBetween {
options.suffix += ct.newLine
}
if len(insert) == 1 {
ct.insertNodeAt(sourceFile, core.TextPos(pos), insert[0], options)
} else {
ct.insertNodesAt(sourceFile, core.TextPos(pos), insert, options)
}
}
func (ct *changeTracker) getInsertNodeAfterOptions(sourceFile *ast.SourceFile, node *ast.Node) changeNodeOptions {
newLineChar := ct.newLine
var options changeNodeOptions
switch node.Kind {
case ast.KindParameter:
// default opts
options = changeNodeOptions{}
case ast.KindClassDeclaration, ast.KindModuleDeclaration:
options = changeNodeOptions{prefix: newLineChar, suffix: newLineChar}
case ast.KindVariableDeclaration, ast.KindStringLiteral, ast.KindIdentifier:
options = changeNodeOptions{prefix: ", "}
case ast.KindPropertyAssignment:
options = changeNodeOptions{suffix: "," + newLineChar}
case ast.KindExportKeyword:
options = changeNodeOptions{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 = changeNodeOptions{suffix: newLineChar}
}
if node.End() == sourceFile.End() && ast.IsStatement(node) {
options.prefix = "\n" + options.prefix
}
return options
}

View File

@ -1,413 +0,0 @@
package ls
import (
"fmt"
"slices"
"strings"
"unicode"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/format"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/parser"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
)
func (ct *changeTracker) getTextChangesFromChanges() map[string][]*lsproto.TextEdit {
changes := map[string][]*lsproto.TextEdit{}
for sourceFile, changesInFile := range ct.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 CompareRanges(ptrTo(a.Range), ptrTo(b.Range)) })
// verify that change intervals do not overlap, except possibly at end points.
for i := range len(changesInFile) - 1 {
if 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 := ct.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 (ct *changeTracker) computeNewText(change *trackerEdit, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile) string {
switch change.kind {
case trackerEditKindRemove:
return ""
case trackerEditKindText:
return change.NewText
}
pos := int(ct.ls.converters.LineAndCharacterToPosition(sourceFile, change.Range.Start))
formatNode := func(n *ast.Node) string {
return ct.getFormattedTextOfNode(n, targetSourceFile, sourceFile, pos, change.options)
}
var text string
switch change.kind {
case trackerEditKindReplaceWithMultipleNodes:
if change.options.joiner == "" {
change.options.joiner = ct.newLine
}
text = strings.Join(core.Map(change.nodes, func(n *ast.Node) string { return strings.TrimSuffix(formatNode(n), ct.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 && *change.options.indentation != 0 || format.GetLineStartPositionForPosition(pos, targetSourceFile) == pos) {
noIndent = strings.TrimLeftFunc(text, unicode.IsSpace)
}
return change.options.prefix + noIndent // !!! +((!options.suffix || endsWith(noIndent, options.suffix)) ? "" : options.suffix);
}
/** Note: this may mutate `nodeIn`. */
func (ct *changeTracker) getFormattedTextOfNode(nodeIn *ast.Node, targetSourceFile *ast.SourceFile, sourceFile *ast.SourceFile, pos int, options changeNodeOptions) string {
text, sourceFileLike := ct.getNonformattedText(nodeIn, targetSourceFile)
// !!! if (validate) validate(node, text);
formatOptions := getFormatCodeSettingsForWriting(ct.formatSettings, targetSourceFile)
var initialIndentation, delta int
if options.indentation == nil {
// !!! indentation for position
// initialIndentation = format.GetIndentationForPos(pos, sourceFile, formatOptions, options.prefix == ct.newLine || scanner.GetLineStartPositionForPosition(pos, targetFileLineMap) == 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(ct.ctx, sourceFileLike, sourceFileLike.AsSourceFile(), targetSourceFile.LanguageVariant, initialIndentation, delta)
return core.ApplyBulkEdits(text, changes)
}
func getFormatCodeSettingsForWriting(options *format.FormatCodeSettings, sourceFile *ast.SourceFile) *format.FormatCodeSettings {
shouldAutoDetectSemicolonPreference := options.Semicolons == format.SemicolonPreferenceIgnore
shouldRemoveSemicolons := options.Semicolons == format.SemicolonPreferenceRemove || shouldAutoDetectSemicolonPreference && !probablyUsesSemicolons(sourceFile)
if shouldRemoveSemicolons {
options.Semicolons = format.SemicolonPreferenceRemove
}
return options
}
/** Note: output node may be mutated input node. */
func (ct *changeTracker) getNonformattedText(node *ast.Node, sourceFile *ast.SourceFile) (string, *ast.Node) {
nodeIn := node
eofToken := ct.Factory.NewToken(ast.KindEndOfFile)
if ast.IsStatement(node) {
nodeIn = ct.Factory.NewSourceFile(
ast.SourceFileParseOptions{FileName: sourceFile.FileName(), Path: sourceFile.Path()},
"",
ct.Factory.NewNodeList([]*ast.Node{node}),
ct.Factory.NewToken(ast.KindEndOfFile),
)
}
writer := printer.NewChangeTrackerWriter(ct.newLine)
printer.NewPrinter(
printer.PrinterOptions{
NewLine: core.GetNewLineKind(ct.newLine),
NeverAsciiEscape: true,
PreserveSourceNewlines: true,
TerminateUnterminatedLiterals: true,
},
writer.GetPrintHandlers(),
ct.EmitContext,
).Write(nodeIn, sourceFile, writer, nil)
text := writer.String()
nodeOut := writer.AssignPositionsToNode(nodeIn, ct.NodeFactory)
var sourceFileLike *ast.Node
if !ast.IsStatement(node) {
nodeList := ct.Factory.NewNodeList([]*ast.Node{nodeOut})
nodeList.Loc = nodeOut.Loc
eofToken.Loc = core.NewTextRange(nodeOut.End(), nodeOut.End())
sourceFileLike = ct.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
} else {
sourceFileLike = nodeOut
}
return text, sourceFileLike
}
// method on the changeTracker because use of converters
func (ct *changeTracker) getAdjustedRange(sourceFile *ast.SourceFile, startNode *ast.Node, endNode *ast.Node, leadingOption leadingTriviaOption, trailingOption trailingTriviaOption) lsproto.Range {
return *ct.ls.createLspRangeFromBounds(
ct.getAdjustedStartPosition(sourceFile, startNode, leadingOption, false),
ct.getAdjustedEndPosition(sourceFile, endNode, trailingOption),
sourceFile,
)
}
// method on the changeTracker because use of converters
func (ct *changeTracker) getAdjustedStartPosition(sourceFile *ast.SourceFile, node *ast.Node, leadingOption leadingTriviaOption, hasTrailingComment bool) int {
if leadingOption == leadingTriviaOptionJSDoc {
if JSDocComments := parser.GetJSDocCommentRanges(ct.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(ct.NodeFactory, sourceFile.Text(), fullStart))
if len(comments) == 0 {
comments = slices.Collect(scanner.GetTrailingCommentRanges(ct.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 (ct *changeTracker) 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(ct.NodeFactory, sourceFile.Text(), node.End()) {
// Single line can break the loop as trivia will only be this line.
// Comments on subsequest 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 (ct *changeTracker) 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(ct.NodeFactory, sourceFile.Text(), node.End())),
scanner.GetLeadingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End()),
); len(comments) > 0 {
if realEnd := comments[len(comments)-1].End(); realEnd != 0 {
return realEnd
}
}
return node.End()
}
if multilineEndPosition := ct.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 (ct *changeTracker) 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(ct.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
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
package ls
const (
moduleSpecifierResolutionLimit = 100
moduleSpecifierResolutionCacheAttemptLimit = 1000
)

View File

@ -1,199 +0,0 @@
package ls
import (
"fmt"
"net/url"
"slices"
"strings"
"unicode/utf16"
"unicode/utf8"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/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 strings.HasPrefix(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)
if line < 0 || int(line) >= len(lineMap.LineStarts) {
panic(fmt.Sprintf("bad line number. Line: %d, lineMap length: %d", line, len(lineMap.LineStarts)))
}
start := lineMap.LineStarts[line]
if lineMap.AsciiOnly || c.positionEncoding == lsproto.PositionEncodingKindUTF8 {
return start + char
}
var utf8Char core.TextPos
var utf16Char core.TextPos
for i, r := range script.Text()[start:] {
u16Len := core.TextPos(utf16.RuneLen(r))
if utf16Char+u16Len > char {
break
}
utf16Char += u16Len
utf8Char = core.TextPos(i + utf8.RuneLen(r))
}
return start + utf8Char
}
func (c *Converters) PositionToLineAndCharacter(script Script, position core.TextPos) lsproto.Position {
// UTF-8 offset to UTF-8/16 0-indexed line and character
lineMap := c.getLineMap(script.FileName())
line, isLineStart := slices.BinarySearch(lineMap.LineStarts, position)
if !isLineStart {
line--
}
line = max(0, line)
// 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),
}
}
func ptrTo[T any](v T) *T {
return &v
}

View File

@ -1,83 +0,0 @@
package ls_test
import (
"testing"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ls"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/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, ls.FileNameToDocumentURI(test.fileName), test.uri)
})
}
}

View File

@ -1,234 +0,0 @@
package ls
import (
"context"
"slices"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
)
func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) {
program, file := l.getProgramAndFile(documentURI)
node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position)))
if node.Kind == ast.KindSourceFile {
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil
}
c, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
if node.Kind == ast.KindOverrideKeyword {
if sym := getSymbolForOverriddenMember(c, node); sym != nil {
return l.createLocationsFromDeclarations(sym.Declarations), nil
}
}
if ast.IsJumpStatementTarget(node) {
if label := getTargetLabel(node.Parent, node.Text()); label != nil {
return l.createLocationsFromDeclarations([]*ast.Node{label}), 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.createLocationsFromDeclarations([]*ast.Node{fn}), nil
}
}
declarations := getDeclarationsFromLocation(c, node)
calledDeclaration := tryGetSignatureDeclaration(c, node)
if calledDeclaration != nil {
// If we can resolve a call signature, remove all function-like declarations and add that signature.
nonFunctionDeclarations := core.Filter(slices.Clip(declarations), func(node *ast.Node) bool { return !ast.IsFunctionLike(node) })
declarations = append(nonFunctionDeclarations, calledDeclaration)
}
return l.createLocationsFromDeclarations(declarations), nil
}
func (l *LanguageService) ProvideTypeDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) {
program, file := l.getProgramAndFile(documentURI)
node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position)))
if node.Kind == ast.KindSourceFile {
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{}, nil
}
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.createLocationsFromDeclarations(declarations), nil
}
if symbol.Flags&ast.SymbolFlagsValue == 0 && symbol.Flags&ast.SymbolFlagsType != 0 {
return l.createLocationsFromDeclarations(symbol.Declarations), 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
}
func (l *LanguageService) createLocationsFromDeclarations(declarations []*ast.Node) lsproto.DefinitionResponse {
locations := make([]lsproto.Location, 0, len(declarations))
for _, decl := range declarations {
file := ast.GetSourceFileOfNode(decl)
name := core.OrElse(ast.GetNameOfDeclaration(decl), decl)
nodeRange := createRangeFromNode(name, file)
mappedLocation := l.getMappedLocation(file.FileName(), nodeRange)
locations = core.AppendIfUnique(locations, mappedLocation)
}
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) {
return c.GetResolvedSymbol(node).Declarations
}
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
}
}
if symbol.Flags&(ast.SymbolFlagsProperty|ast.SymbolFlagsMethod|ast.SymbolFlagsAccessor) != 0 && symbol.Parent != nil && symbol.Parent.Flags&ast.SymbolFlagsObjectLiteral != 0 {
if objectLiteral := core.FirstOrNil(symbol.Parent.Declarations); objectLiteral != nil {
if declarations := c.GetContextualDeclarationsForObjectLiteralElement(objectLiteral, symbol.Name); len(declarations) != 0 {
return declarations
}
}
}
return symbol.Declarations
}
if indexInfos := c.GetIndexSignaturesAtLocation(node); len(indexInfos) != 0 {
return indexInfos
}
return nil
}
// 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 !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 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
}

View File

@ -1,109 +0,0 @@
package ls
import (
"context"
"strings"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnostics"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/diagnosticwriter"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
)
func (l *LanguageService) ProvideDiagnostics(ctx context.Context, uri lsproto.DocumentUri) (lsproto.DocumentDiagnosticResponse, error) {
program, file := l.getProgramAndFile(uri)
diagnostics := make([][]*ast.Diagnostic, 0, 4)
diagnostics = append(diagnostics, program.GetSyntacticDiagnostics(ctx, file))
diagnostics = append(diagnostics, program.GetSemanticDiagnostics(ctx, file))
// !!! user preference for suggestion diagnostics; keep only unnecessary/deprecated?
// See: https://github.com/microsoft/vscode/blob/3dbc74129aaae102e5cb485b958fa5360e8d3e7a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts#L114
diagnostics = append(diagnostics, program.GetSuggestionDiagnostics(ctx, file))
if program.Options().GetEmitDeclarations() {
diagnostics = append(diagnostics, program.GetDeclarationDiagnostics(ctx, file))
}
return lsproto.RelatedFullDocumentDiagnosticReportOrUnchangedDocumentDiagnosticReport{
FullDocumentDiagnosticReport: &lsproto.RelatedFullDocumentDiagnosticReport{
Items: toLSPDiagnostics(l.converters, diagnostics...),
},
}, nil
}
func toLSPDiagnostics(converters *Converters, 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, toLSPDiagnostic(converters, diag))
}
}
return lspDiagnostics
}
func toLSPDiagnostic(converters *Converters, diagnostic *ast.Diagnostic) *lsproto.Diagnostic {
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
}
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.Message(),
})
}
var tags []lsproto.DiagnosticTag
if diagnostic.ReportsUnnecessary() || diagnostic.ReportsDeprecated() {
tags = make([]lsproto.DiagnosticTag, 0, 2)
if diagnostic.ReportsUnnecessary() {
tags = append(tags, lsproto.DiagnosticTagUnnecessary)
}
if diagnostic.ReportsDeprecated() {
tags = append(tags, lsproto.DiagnosticTagDeprecated)
}
}
return &lsproto.Diagnostic{
Range: converters.ToLSPRange(diagnostic.File(), diagnostic.Loc()),
Code: &lsproto.IntegerOrString{
Integer: ptrTo(diagnostic.Code()),
},
Severity: &severity,
Message: messageChainToString(diagnostic),
Source: ptrTo("ts"),
RelatedInformation: ptrToSliceIfNonEmpty(relatedInformation),
Tags: ptrToSliceIfNonEmpty(tags),
}
}
func messageChainToString(diagnostic *ast.Diagnostic) string {
if len(diagnostic.MessageChain()) == 0 {
return diagnostic.Message()
}
var b strings.Builder
diagnosticwriter.WriteFlattenedDiagnosticMessage(&b, diagnostic, "\n")
return b.String()
}
func ptrToSliceIfNonEmpty[T any](s []T) *[]T {
if len(s) == 0 {
return nil
}
return &s
}

View File

@ -1,690 +0,0 @@
package ls
import (
"context"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsutil"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
)
func (l *LanguageService) ProvideDocumentHighlights(ctx context.Context, documentUri lsproto.DocumentUri, documentPosition lsproto.Position) (lsproto.DocumentHighlightResponse, error) {
program, sourceFile := l.getProgramAndFile(documentUri)
position := int(l.converters.LineAndCharacterToPosition(sourceFile, documentPosition))
node := astnav.GetTouchingPropertyName(sourceFile, position)
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 documentHighlights []*lsproto.DocumentHighlight
kind := lsproto.DocumentHighlightKindRead
if openingElement != nil {
documentHighlights = append(documentHighlights, &lsproto.DocumentHighlight{
Range: *l.createLspRangeFromNode(openingElement, sourceFile),
Kind: &kind,
})
}
if closingElement != nil {
documentHighlights = append(documentHighlights, &lsproto.DocumentHighlight{
Range: *l.createLspRangeFromNode(closingElement, sourceFile),
Kind: &kind,
})
}
return lsproto.DocumentHighlightsOrNull{
DocumentHighlights: &documentHighlights,
}, nil
}
documentHighlights := l.getSemanticDocumentHighlights(ctx, position, node, program, sourceFile)
if len(documentHighlights) == 0 {
documentHighlights = l.getSyntacticDocumentHighlights(node, sourceFile)
}
// if nil is passed here we never generate an error, just pass an empty higlight
return lsproto.DocumentHighlightsOrNull{DocumentHighlights: &documentHighlights}, nil
}
func (l *LanguageService) getSemanticDocumentHighlights(ctx context.Context, position int, node *ast.Node, program *compiler.Program, sourceFile *ast.SourceFile) []*lsproto.DocumentHighlight {
options := refOptions{use: referenceUseReferences}
referenceEntries := l.getReferencedSymbolsForNode(ctx, position, node, program, []*ast.SourceFile{sourceFile}, options, &collections.Set[string]{})
if referenceEntries == nil {
return nil
}
var highlights []*lsproto.DocumentHighlight
for _, entry := range referenceEntries {
for _, ref := range entry.references {
if ref.node != nil {
fileName, highlight := l.toDocumentHighlight(ref)
if fileName == sourceFile.FileName() {
highlights = append(highlights, highlight)
}
}
}
}
return highlights
}
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: *entry.textRange,
Kind: &kind,
}
}
// Determine write access for node references.
if ast.IsWriteAccessForReference(entry.node) {
kind = lsproto.DocumentHighlightKindWrite
}
dh := &lsproto.DocumentHighlight{
Range: *entry.textRange,
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 {
// Traverse upwards through all parent if-statements linked by their else-branches.
// Is this cast error safe or should i be checking if elseStatement exists first?
for ast.IsIfStatement(ifStatement.Parent) && ifStatement.Parent.AsIfStatement().ElseStatement.AsIfStatement() == ifStatement {
ifStatement = ifStatement.Parent.AsIfStatement()
}
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 := 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 := 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 := 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 := 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.Kind == ast.KindTryKeyword {
keywords = append(keywords, token)
}
if tryStatement.CatchClause != nil {
catchToken := lsutil.GetFirstToken(tryStatement.CatchClause.AsNode(), sourceFile)
if catchToken.Kind == ast.KindCatchKeyword {
keywords = append(keywords, catchToken)
}
}
if tryStatement.FinallyBlock != nil {
finallyKeyword := findChildOfKind(node, ast.KindFinallyKeyword, sourceFile)
if finallyKeyword.Kind == ast.KindFinallyKeyword {
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
modifiers := fun.Modifiers()
if modifiers != nil {
for _, modifier := range modifiers.Nodes {
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).AsFunctionDeclaration()
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 {
if modifiers := node.Modifiers(); modifiers != nil {
for _, modifier := range modifiers.Nodes {
if modifier.Kind == kind {
return modifier
}
}
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,181 +0,0 @@
package ls
import (
"context"
"iter"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/format"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
)
func toFormatCodeSettings(opt *lsproto.FormattingOptions, newLine string) *format.FormatCodeSettings {
initial := format.GetDefaultFormatCodeSettings(newLine)
initial.TabSize = int(opt.TabSize)
initial.IndentSize = int(opt.TabSize)
initial.ConvertTabsToSpaces = opt.InsertSpaces
if opt.TrimTrailingWhitespace != nil {
initial.TrimTrailingWhitespace = *opt.TrimTrailingWhitespace
}
// !!! get format settings
// TODO: We support a _lot_ more options than this
return initial
}
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) {
_, file := l.getProgramAndFile(documentURI)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForDocument(
ctx,
file,
toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()),
))
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) {
_, file := l.getProgramAndFile(documentURI)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsForRange(
ctx,
file,
toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()),
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) {
_, file := l.getProgramAndFile(documentURI)
edits := l.toLSProtoTextEdits(file, l.getFormattingEditsAfterKeystroke(
ctx,
file,
toFormatCodeSettings(options, l.GetProgram().Options().NewLine.GetNewLineCharacter()),
int(l.converters.LineAndCharacterToPosition(file, position)),
character,
))
return lsproto.TextEditsOrNull{TextEdits: &edits}, nil
}
func (l *LanguageService) getFormattingEditsForRange(
ctx context.Context,
file *ast.SourceFile,
options *format.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 *format.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 *format.FormatCodeSettings,
position int,
key string,
) []core.TextChange {
ctx = format.WithFormatCodeSettings(ctx, options, options.NewLineCharacter)
if isInComment(file, position, nil) == 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
}

View File

@ -1,10 +0,0 @@
package ls
import "efprojects.com/kitten-ipc/kitcom/internal/tsgo/sourcemap"
type Host interface {
UseCaseSensitiveFileNames() bool
ReadFile(path string) (contents string, ok bool)
Converters() *Converters
GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo
}

View File

@ -1,488 +0,0 @@
package ls
import (
"context"
"fmt"
"slices"
"strings"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
)
const (
symbolFormatFlags = checker.SymbolFormatFlagsWriteTypeParametersOrArguments | checker.SymbolFormatFlagsUseOnlyExternalAliasing | checker.SymbolFormatFlagsAllowAnyNodeKind | checker.SymbolFormatFlagsUseAliasDefinedOutsideCurrentScope
typeFormatFlags = checker.TypeFormatFlagsUseAliasDefinedOutsideCurrentScope
)
func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.HoverResponse, error) {
program, file := l.getProgramAndFile(documentURI)
node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position)))
if node.Kind == ast.KindSourceFile {
// Avoid giving quickInfo for the sourceFile as a whole.
return lsproto.HoverOrNull{}, nil
}
c, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
quickInfo, documentation := getQuickInfoAndDocumentation(c, node)
if quickInfo == "" {
return lsproto.HoverOrNull{}, nil
}
return lsproto.HoverOrNull{
Hover: &lsproto.Hover{
Contents: lsproto.MarkupContentOrStringOrMarkedStringWithLanguageOrMarkedStrings{
MarkupContent: &lsproto.MarkupContent{
Kind: lsproto.MarkupKindMarkdown,
Value: formatQuickInfo(quickInfo) + documentation,
},
},
},
}, nil
}
func getQuickInfoAndDocumentation(c *checker.Checker, node *ast.Node) (string, string) {
return getQuickInfoAndDocumentationForSymbol(c, c.GetSymbolAtLocation(node), getNodeForQuickInfo(node))
}
func getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) (string, string) {
quickInfo, declaration := getQuickInfoAndDeclarationAtLocation(c, symbol, node)
if quickInfo == "" {
return "", ""
}
var b strings.Builder
if declaration != nil {
if jsdoc := getJSDocOrTag(declaration); jsdoc != nil && !containsTypedefTag(jsdoc) {
writeComments(&b, jsdoc.Comments())
if jsdoc.Kind == ast.KindJSDoc {
if tags := jsdoc.AsJSDoc().Tags; tags != nil {
for _, tag := range tags.Nodes {
if tag.Kind == ast.KindJSDocTypeTag {
continue
}
b.WriteString("\n\n*@")
b.WriteString(tag.TagName().Text())
b.WriteString("*")
switch tag.Kind {
case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag:
writeOptionalEntityName(&b, tag.Name())
case ast.KindJSDocAugmentsTag:
writeOptionalEntityName(&b, tag.AsJSDocAugmentsTag().ClassName)
case ast.KindJSDocSeeTag:
writeOptionalEntityName(&b, tag.AsJSDocSeeTag().NameExpression)
case ast.KindJSDocTemplateTag:
for i, tp := range tag.TypeParameters() {
if i != 0 {
b.WriteString(",")
}
writeOptionalEntityName(&b, tp.Name())
}
}
comments := tag.Comments()
if len(comments) != 0 {
if commentHasPrefix(comments, "```") {
b.WriteString("\n")
} else {
b.WriteString(" ")
if !commentHasPrefix(comments, "-") {
b.WriteString("— ")
}
}
writeComments(&b, comments)
}
}
}
}
}
}
return quickInfo, b.String()
}
func formatQuickInfo(quickInfo string) string {
var b strings.Builder
b.Grow(32)
writeCode(&b, "tsx", quickInfo)
return b.String()
}
func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) (string, *ast.Node) {
isAlias := symbol != nil && symbol.Flags&ast.SymbolFlagsAlias != 0
if isAlias {
symbol = c.GetAliasedSymbol(symbol)
}
if symbol == nil || symbol == c.GetUnknownSymbol() {
return "", nil
}
declaration := symbol.ValueDeclaration
if symbol.Flags&ast.SymbolFlagsClass != 0 && inConstructorContext(node) {
if s := symbol.Members[ast.InternalSymbolNameConstructor]; s != nil {
symbol = s
declaration = core.Find(symbol.Declarations, func(d *ast.Node) bool {
return ast.IsConstructorDeclaration(d) || ast.IsConstructSignatureDeclaration(d)
})
}
}
flags := symbol.Flags
if flags&ast.SymbolFlagsType != 0 && (ast.IsPartOfTypeNode(node) || ast.IsTypeDeclarationName(node)) {
// If the symbol has a type meaning and we're in a type context, remove value-only meanings
flags &^= ast.SymbolFlagsVariable | ast.SymbolFlagsFunction
}
container := getContainerNode(node)
var b strings.Builder
if isAlias {
b.WriteString("(alias) ")
}
switch {
case flags&(ast.SymbolFlagsVariable|ast.SymbolFlagsProperty|ast.SymbolFlagsAccessor) != 0:
switch {
case flags&ast.SymbolFlagsProperty != 0:
b.WriteString("(property) ")
case flags&ast.SymbolFlagsAccessor != 0:
b.WriteString("(accessor) ")
default:
decl := symbol.ValueDeclaration
if decl != nil {
switch {
case ast.IsParameter(decl):
b.WriteString("(parameter) ")
case ast.IsVarLet(decl):
b.WriteString("let ")
case ast.IsVarConst(decl):
b.WriteString("const ")
case ast.IsVarUsing(decl):
b.WriteString("using ")
case ast.IsVarAwaitUsing(decl):
b.WriteString("await using ")
default:
b.WriteString("var ")
}
}
}
b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags))
b.WriteString(": ")
b.WriteString(c.TypeToStringEx(c.GetTypeOfSymbolAtLocation(symbol, node), container, typeFormatFlags))
case flags&ast.SymbolFlagsEnumMember != 0:
b.WriteString("(enum member) ")
t := c.GetTypeOfSymbol(symbol)
b.WriteString(c.TypeToStringEx(t, container, typeFormatFlags))
if t.Flags()&checker.TypeFlagsLiteral != 0 {
b.WriteString(" = ")
b.WriteString(t.AsLiteralType().String())
}
case flags&(ast.SymbolFlagsFunction|ast.SymbolFlagsMethod) != 0:
signatures := getSignaturesAtLocation(c, symbol, checker.SignatureKindCall, node)
if len(signatures) == 1 && signatures[0].Declaration() != nil {
declaration = signatures[0].Declaration()
}
prefix := core.IfElse(symbol.Flags&ast.SymbolFlagsMethod != 0, "(method) ", "function ")
writeSignatures(&b, c, signatures, container, prefix, symbol)
case flags&ast.SymbolFlagsConstructor != 0:
signatures := getSignaturesAtLocation(c, symbol.Parent, checker.SignatureKindConstruct, node)
if len(signatures) == 1 && signatures[0].Declaration() != nil {
declaration = signatures[0].Declaration()
}
writeSignatures(&b, c, signatures, container, "constructor ", symbol.Parent)
case flags&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) != 0:
if node.Kind == ast.KindThisKeyword || ast.IsThisInTypeQuery(node) {
b.WriteString("this")
} else {
b.WriteString(core.IfElse(symbol.Flags&ast.SymbolFlagsClass != 0, "class ", "interface "))
b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags))
params := c.GetDeclaredTypeOfSymbol(symbol).AsInterfaceType().LocalTypeParameters()
writeTypeParams(&b, c, params)
}
if flags&ast.SymbolFlagsInterface != 0 {
declaration = core.Find(symbol.Declarations, ast.IsInterfaceDeclaration)
}
case flags&ast.SymbolFlagsEnum != 0:
b.WriteString("enum ")
b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags))
case flags&ast.SymbolFlagsModule != 0:
b.WriteString(core.IfElse(symbol.ValueDeclaration != nil && ast.IsSourceFile(symbol.ValueDeclaration), "module ", "namespace "))
b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags))
case flags&ast.SymbolFlagsTypeParameter != 0:
b.WriteString("(type parameter) ")
tp := c.GetDeclaredTypeOfSymbol(symbol)
b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags))
cons := c.GetConstraintOfTypeParameter(tp)
if cons != nil {
b.WriteString(" extends ")
b.WriteString(c.TypeToStringEx(cons, container, typeFormatFlags))
}
declaration = core.Find(symbol.Declarations, ast.IsTypeParameterDeclaration)
case flags&ast.SymbolFlagsTypeAlias != 0:
b.WriteString("type ")
b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags))
writeTypeParams(&b, c, c.GetTypeAliasTypeParameters(symbol))
if len(symbol.Declarations) != 0 {
b.WriteString(" = ")
b.WriteString(c.TypeToStringEx(c.GetDeclaredTypeOfSymbol(symbol), container, typeFormatFlags|checker.TypeFormatFlagsInTypeAlias))
}
declaration = core.Find(symbol.Declarations, ast.IsTypeAliasDeclaration)
case flags&ast.SymbolFlagsAlias != 0:
b.WriteString("import ")
b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags))
default:
b.WriteString(c.TypeToStringEx(c.GetTypeOfSymbol(symbol), container, typeFormatFlags))
}
return b.String(), declaration
}
func getNodeForQuickInfo(node *ast.Node) *ast.Node {
if node.Parent == nil {
return node
}
if ast.IsNewExpression(node.Parent) && node.Pos() == node.Parent.Pos() {
return node.Parent.Expression()
}
if ast.IsNamedTupleMember(node.Parent) && node.Pos() == node.Parent.Pos() {
return node.Parent
}
if ast.IsImportMeta(node.Parent) && node.Parent.Name() == node {
return node.Parent
}
if ast.IsJsxNamespacedName(node.Parent) {
return node.Parent
}
return node
}
func inConstructorContext(node *ast.Node) bool {
if node.Kind == ast.KindConstructorKeyword {
return true
}
if ast.IsIdentifier(node) {
for ast.IsRightSideOfQualifiedNameOrPropertyAccess(node) {
node = node.Parent
}
if ast.IsNewExpression(node.Parent) {
return true
}
}
return false
}
func getSignaturesAtLocation(c *checker.Checker, symbol *ast.Symbol, kind checker.SignatureKind, node *ast.Node) []*checker.Signature {
signatures := c.GetSignaturesOfType(c.GetTypeOfSymbol(symbol), kind)
if len(signatures) > 1 || len(signatures) == 1 && len(signatures[0].TypeParameters()) != 0 {
if callNode := getCallOrNewExpression(node); callNode != nil {
signature := c.GetResolvedSignature(callNode)
// If we have a resolved signature, make sure it isn't a synthetic signature
if signature != nil && (slices.Contains(signatures, signature) || signature.Target() != nil && slices.Contains(signatures, signature.Target())) {
return []*checker.Signature{signature}
}
}
}
return signatures
}
func getCallOrNewExpression(node *ast.Node) *ast.Node {
if ast.IsSourceFile(node) {
return nil
}
if ast.IsPropertyAccessExpression(node.Parent) && node.Parent.Name() == node {
node = node.Parent
}
if ast.IsCallExpression(node.Parent) || ast.IsNewExpression(node.Parent) {
return node.Parent
}
return nil
}
func writeTypeParams(b *strings.Builder, c *checker.Checker, params []*checker.Type) {
if len(params) > 0 {
b.WriteString("<")
for i, tp := range params {
if i != 0 {
b.WriteString(", ")
}
symbol := tp.Symbol()
b.WriteString(c.SymbolToStringEx(symbol, nil, ast.SymbolFlagsNone, symbolFormatFlags))
cons := c.GetConstraintOfTypeParameter(tp)
if cons != nil {
b.WriteString(" extends ")
b.WriteString(c.TypeToStringEx(cons, nil, typeFormatFlags))
}
}
b.WriteString(">")
}
}
func writeSignatures(b *strings.Builder, c *checker.Checker, signatures []*checker.Signature, container *ast.Node, prefix string, symbol *ast.Symbol) {
for i, sig := range signatures {
if i != 0 {
b.WriteString("\n")
}
if i == 3 && len(signatures) >= 5 {
b.WriteString(fmt.Sprintf("// +%v more overloads", len(signatures)-3))
break
}
b.WriteString(prefix)
b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags))
b.WriteString(c.SignatureToStringEx(sig, container, typeFormatFlags|checker.TypeFormatFlagsWriteCallStyleSignature|checker.TypeFormatFlagsWriteTypeArgumentsOfSignature))
}
}
func containsTypedefTag(jsdoc *ast.Node) bool {
if jsdoc.Kind == ast.KindJSDoc {
if tags := jsdoc.AsJSDoc().Tags; tags != nil {
for _, tag := range tags.Nodes {
if tag.Kind == ast.KindJSDocTypedefTag {
return true
}
}
}
}
return false
}
func commentHasPrefix(comments []*ast.Node, prefix string) bool {
return comments[0].Kind == ast.KindJSDocText && strings.HasPrefix(comments[0].Text(), prefix)
}
func getJSDoc(node *ast.Node) *ast.Node {
return core.LastOrNil(node.JSDoc(nil))
}
func getJSDocOrTag(node *ast.Node) *ast.Node {
if jsdoc := getJSDoc(node); jsdoc != nil {
return jsdoc
}
switch {
case ast.IsParameter(node):
return getMatchingJSDocTag(node.Parent, node.Name().Text(), isMatchingParameterTag)
case ast.IsTypeParameterDeclaration(node):
return getMatchingJSDocTag(node.Parent, node.Name().Text(), isMatchingTemplateTag)
case ast.IsVariableDeclaration(node) && ast.IsVariableDeclarationList(node.Parent) && core.FirstOrNil(node.Parent.AsVariableDeclarationList().Declarations.Nodes) == node:
return getJSDocOrTag(node.Parent.Parent)
case (ast.IsFunctionExpressionOrArrowFunction(node) || ast.IsClassExpression(node)) &&
(ast.IsVariableDeclaration(node.Parent) || ast.IsPropertyDeclaration(node.Parent) || ast.IsPropertyAssignment(node.Parent)) && node.Parent.Initializer() == node:
return getJSDocOrTag(node.Parent)
}
return nil
}
func getMatchingJSDocTag(node *ast.Node, name string, match func(*ast.Node, string) bool) *ast.Node {
if jsdoc := getJSDocOrTag(node); jsdoc != nil && jsdoc.Kind == ast.KindJSDoc {
if tags := jsdoc.AsJSDoc().Tags; tags != nil {
for _, tag := range tags.Nodes {
if match(tag, name) {
return tag
}
}
}
}
return nil
}
func isMatchingParameterTag(tag *ast.Node, name string) bool {
return tag.Kind == ast.KindJSDocParameterTag && isNodeWithName(tag, name)
}
func isMatchingTemplateTag(tag *ast.Node, name string) bool {
return tag.Kind == ast.KindJSDocTemplateTag && core.Some(tag.TypeParameters(), func(tp *ast.Node) bool { return isNodeWithName(tp, name) })
}
func isNodeWithName(node *ast.Node, name string) bool {
nodeName := node.Name()
return ast.IsIdentifier(nodeName) && nodeName.Text() == name
}
func writeCode(b *strings.Builder, lang string, code string) {
if code == "" {
return
}
ticks := 3
for strings.Contains(code, strings.Repeat("`", ticks)) {
ticks++
}
for range ticks {
b.WriteByte('`')
}
b.WriteString(lang)
b.WriteByte('\n')
b.WriteString(code)
b.WriteByte('\n')
for range ticks {
b.WriteByte('`')
}
b.WriteByte('\n')
}
func writeComments(b *strings.Builder, comments []*ast.Node) {
for _, comment := range comments {
switch comment.Kind {
case ast.KindJSDocText:
b.WriteString(comment.Text())
case ast.KindJSDocLink:
name := comment.Name()
text := comment.AsJSDocLink().Text()
if name != nil {
if text == "" {
writeEntityName(b, name)
} else {
writeEntityNameParts(b, name)
}
}
b.WriteString(text)
case ast.KindJSDocLinkCode:
// !!! TODO: This is a temporary placeholder implementation that needs to be updated later
name := comment.Name()
text := comment.AsJSDocLinkCode().Text()
if name != nil {
if text == "" {
writeEntityName(b, name)
} else {
writeEntityNameParts(b, name)
}
}
b.WriteString(text)
case ast.KindJSDocLinkPlain:
// !!! TODO: This is a temporary placeholder implementation that needs to be updated later
name := comment.Name()
text := comment.AsJSDocLinkPlain().Text()
if name != nil {
if text == "" {
writeEntityName(b, name)
} else {
writeEntityNameParts(b, name)
}
}
b.WriteString(text)
}
}
}
func writeOptionalEntityName(b *strings.Builder, name *ast.Node) {
if name != nil {
b.WriteString(" ")
writeEntityName(b, name)
}
}
func writeEntityName(b *strings.Builder, name *ast.Node) {
b.WriteString("`")
writeEntityNameParts(b, name)
b.WriteString("`")
}
func writeEntityNameParts(b *strings.Builder, node *ast.Node) {
switch node.Kind {
case ast.KindIdentifier:
b.WriteString(node.Text())
case ast.KindQualifiedName:
writeEntityNameParts(b, node.AsQualifiedName().Left)
b.WriteByte('.')
writeEntityNameParts(b, node.AsQualifiedName().Right)
case ast.KindPropertyAccessExpression:
writeEntityNameParts(b, node.Expression())
b.WriteByte('.')
writeEntityNameParts(b, node.Name())
case ast.KindParenthesizedExpression, ast.KindExpressionWithTypeArguments:
writeEntityNameParts(b, node.Expression())
case ast.KindJSDocNameReference:
writeEntityNameParts(b, node.Name())
}
}

View File

@ -1,728 +0,0 @@
package ls
import (
"slices"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug"
)
type ImpExpKind int32
const (
ImpExpKindUnknown ImpExpKind = iota
ImpExpKindImport
ImpExpKindExport
)
type ImportExportSymbol struct {
kind ImpExpKind
symbol *ast.Symbol
exportInfo *ExportInfo
}
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(sourceFiles []*ast.SourceFile, sourceFilesSet *collections.Set[string], checker *checker.Checker) ImportTracker {
allDirectImports := getDirectImportsMap(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(sourceFiles []*ast.SourceFile, checker *checker.Checker) map[*ast.Symbol][]*ast.Node {
result := make(map[*ast.Symbol][]*ast.Node)
for _, sourceFile := range sourceFiles {
// !!! cancellation
forEachImport(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(sourceFile *ast.SourceFile, action func(importStatement *ast.Node, imported *ast.Node)) {
if sourceFile.ExternalModuleIndicator != nil || len(sourceFile.Imports()) != 0 {
for _, i := range sourceFile.Imports() {
action(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 := exportInfo.exportingModuleSymbol.GlobalExports != nil
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) {
debug.Assert(!isAvailableThroughGlobal)
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.AsExportDeclaration().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.AsBinaryExpression()) {
case ast.JSDeclarationKindExportsProperty:
kind = ExportKindNamed
case ast.JSDeclarationKindModuleExports:
kind = ExportKindExportEquals
default:
return nil
}
sym := symbol
if useLhsSymbol {
sym = checker.GetSymbolAtLocation(ast.GetElementOrPropertyAccessName(node.AsBinaryExpression().Left))
}
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):
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
}
// A symbol being imported is always an alias. So get what that aliases to find the local symbol.
importedSymbol := checker.GetImmediateAliasedSymbol(symbol)
if importedSymbol == nil {
return nil
}
// Search on the local symbol in the exporting module, not the exported symbol.
importedSymbol = skipExportSpecifierSymbol(importedSymbol, checker)
// 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.IsVariableDeclarationInitializedToRequire(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.AsBinaryExpression()) == 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 := debug.CheckDefined(importedSymbol.ValueDeclaration)
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(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
}

View File

@ -1,67 +0,0 @@
package ls
import (
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/sourcemap"
)
type LanguageService struct {
host Host
program *compiler.Program
converters *Converters
documentPositionMappers map[string]*sourcemap.DocumentPositionMapper
}
func NewLanguageService(
program *compiler.Program,
host Host,
) *LanguageService {
return &LanguageService{
host: host,
program: program,
converters: host.Converters(),
documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{},
}
}
func (l *LanguageService) GetProgram() *compiler.Program {
return l.program
}
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)
}

View File

@ -1,71 +0,0 @@
package ls
import (
"cmp"
"slices"
"strings"
"unicode/utf8"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/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
}

View File

@ -1,128 +0,0 @@
package ls
import (
"cmp"
"strings"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/modulespecifiers"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
)
// statement = anyImportOrRequireStatement
func getImportDeclarationInsertIndex(sortedImports []*ast.Statement, newImport *ast.Statement, comparer func(a, b *ast.Statement) int) int {
// !!!
return len(sortedImports)
}
// returns `-1` if `a` is better than `b`
//
// note: this sorts in descending order of preference; different than convention in other cmp-like functions
func compareModuleSpecifiers(
a *ImportFix, // !!! ImportFixWithModuleSpecifier
b *ImportFix, // !!! ImportFixWithModuleSpecifier
importingFile *ast.SourceFile, // | FutureSourceFile,
program *compiler.Program,
preferences UserPreferences,
allowsImportingSpecifier func(specifier string) bool,
toPath func(fileName string) tspath.Path,
) int {
if a.kind == ImportFixKindUseNamespace || b.kind == ImportFixKindUseNamespace {
return 0
}
if comparison := compareBooleans(
b.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(b.moduleSpecifier),
a.moduleSpecifierKind != modulespecifiers.ResultKindNodeModules || allowsImportingSpecifier(a.moduleSpecifier),
); comparison != 0 {
return comparison
}
if comparison := compareModuleSpecifierRelativity(a, b, preferences); comparison != 0 {
return comparison
}
if comparison := compareNodeCoreModuleSpecifiers(a.moduleSpecifier, b.moduleSpecifier, importingFile, program); comparison != 0 {
return comparison
}
if comparison := compareBooleans(isFixPossiblyReExportingImportingFile(a, importingFile.Path(), toPath), isFixPossiblyReExportingImportingFile(b, importingFile.Path(), toPath)); comparison != 0 {
return comparison
}
if comparison := compareNumberOfDirectorySeparators(a.moduleSpecifier, b.moduleSpecifier); comparison != 0 {
return comparison
}
return 0
}
// True > False
func compareBooleans(a, b bool) int {
if a && !b {
return -1
} else if !a && b {
return 1
}
return 0
}
// returns `-1` if `a` is better than `b`
func compareModuleSpecifierRelativity(a *ImportFix, b *ImportFix, preferences UserPreferences) int {
switch preferences.ImportModuleSpecifierPreference {
case modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative:
return compareBooleans(a.moduleSpecifierKind == modulespecifiers.ResultKindRelative, b.moduleSpecifierKind == modulespecifiers.ResultKindRelative)
}
return 0
}
func compareNodeCoreModuleSpecifiers(a, b string, importingFile *ast.SourceFile, program *compiler.Program) int {
if strings.HasPrefix(a, "node:") && !strings.HasPrefix(b, "node:") {
if shouldUseUriStyleNodeCoreModules(importingFile, program) {
return -1
}
return 1
}
if strings.HasPrefix(b, "node:") && !strings.HasPrefix(a, "node:") {
if shouldUseUriStyleNodeCoreModules(importingFile, program) {
return 1
}
return -1
}
return 0
}
func shouldUseUriStyleNodeCoreModules(file *ast.SourceFile, program *compiler.Program) bool {
for _, node := range file.Imports() {
if core.NodeCoreModules()[node.Text()] && !core.ExclusivelyPrefixedNodeCoreModules[node.Text()] {
if strings.HasPrefix(node.Text(), "node:") {
return true
} else {
return false
}
}
}
return program.UsesUriStyleNodeCoreModules()
}
// This is a simple heuristic to try to avoid creating an import cycle with a barrel re-export.
// E.g., do not `import { Foo } from ".."` when you could `import { Foo } from "../Foo"`.
// This can produce false positives or negatives if re-exports cross into sibling directories
// (e.g. `export * from "../whatever"`) or are not named "index".
func isFixPossiblyReExportingImportingFile(fix *ImportFix, importingFilePath tspath.Path, toPath func(fileName string) tspath.Path) bool {
if fix.isReExport != nil && *(fix.isReExport) &&
fix.exportInfo != nil && fix.exportInfo.moduleFileName != "" && isIndexFileName(fix.exportInfo.moduleFileName) {
reExportDir := toPath(tspath.GetDirectoryPath(fix.exportInfo.moduleFileName))
return strings.HasPrefix(string(importingFilePath), string(reExportDir))
}
return false
}
func compareNumberOfDirectorySeparators(path1, path2 string) int {
return cmp.Compare(strings.Count(path1, "/"), strings.Count(path2, "/"))
}
func isIndexFileName(fileName string) bool {
fileName = tspath.GetBaseFileName(fileName)
if tspath.FileExtensionIsOneOf(fileName, []string{".js", ".jsx", ".d.ts", ".ts", ".tsx"}) {
fileName = tspath.RemoveFileExtension(fileName)
}
return fileName == "index"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,81 +0,0 @@
package ls
import (
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/debug"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/sourcemap"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/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: FileNameToDocumentURI(fileName),
Range: *lspRange,
}
}
endPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.End()))
debug.Assert(endPos.FileName == startPos.FileName, "start and end should be in same file")
newRange := core.NewTextRange(startPos.Pos, endPos.Pos)
lspRange := l.createLspRangeFromRange(newRange, l.getScript(startPos.FileName))
return lsproto.Location{
Uri: 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
}

View File

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

View File

@ -1,389 +0,0 @@
package ls
import (
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
)
type ScriptElementKind string
const (
ScriptElementKindUnknown ScriptElementKind = ""
ScriptElementKindWarning ScriptElementKind = "warning"
// predefined type (void) or keyword (class)
ScriptElementKindKeyword ScriptElementKind = "keyword"
// top level script node
ScriptElementKindScriptElement ScriptElementKind = "script"
// module foo {}
ScriptElementKindModuleElement ScriptElementKind = "module"
// class X {}
ScriptElementKindClassElement ScriptElementKind = "class"
// var x = class X {}
ScriptElementKindLocalClassElement ScriptElementKind = "local class"
// interface Y {}
ScriptElementKindInterfaceElement ScriptElementKind = "interface"
// type T = ...
ScriptElementKindTypeElement ScriptElementKind = "type"
// enum E {}
ScriptElementKindEnumElement ScriptElementKind = "enum"
ScriptElementKindEnumMemberElement ScriptElementKind = "enum member"
// Inside module and script only.
// const v = ...
ScriptElementKindVariableElement ScriptElementKind = "var"
// Inside function.
ScriptElementKindLocalVariableElement ScriptElementKind = "local var"
// using foo = ...
ScriptElementKindVariableUsingElement ScriptElementKind = "using"
// await using foo = ...
ScriptElementKindVariableAwaitUsingElement ScriptElementKind = "await using"
// Inside module and script only.
// function f() {}
ScriptElementKindFunctionElement ScriptElementKind = "function"
// Inside function.
ScriptElementKindLocalFunctionElement ScriptElementKind = "local function"
// class X { [public|private]* foo() {} }
ScriptElementKindMemberFunctionElement ScriptElementKind = "method"
// class X { [public|private]* [get|set] foo:number; }
ScriptElementKindMemberGetAccessorElement ScriptElementKind = "getter"
ScriptElementKindMemberSetAccessorElement ScriptElementKind = "setter"
// class X { [public|private]* foo:number; }
// interface Y { foo:number; }
ScriptElementKindMemberVariableElement ScriptElementKind = "property"
// class X { [public|private]* accessor foo: number; }
ScriptElementKindMemberAccessorVariableElement ScriptElementKind = "accessor"
// class X { constructor() { } }
// class X { static { } }
ScriptElementKindConstructorImplementationElement ScriptElementKind = "constructor"
// interface Y { ():number; }
ScriptElementKindCallSignatureElement ScriptElementKind = "call"
// interface Y { []:number; }
ScriptElementKindIndexSignatureElement ScriptElementKind = "index"
// interface Y { new():Y; }
ScriptElementKindConstructSignatureElement ScriptElementKind = "construct"
// function foo(*Y*: string)
ScriptElementKindParameterElement ScriptElementKind = "parameter"
ScriptElementKindTypeParameterElement ScriptElementKind = "type parameter"
ScriptElementKindPrimitiveType ScriptElementKind = "primitive type"
ScriptElementKindLabel ScriptElementKind = "label"
ScriptElementKindAlias ScriptElementKind = "alias"
ScriptElementKindConstElement ScriptElementKind = "const"
ScriptElementKindLetElement ScriptElementKind = "let"
ScriptElementKindDirectory ScriptElementKind = "directory"
ScriptElementKindExternalModuleName ScriptElementKind = "external module name"
// String literal
ScriptElementKindString ScriptElementKind = "string"
// Jsdoc @link: in `{@link C link text}`, the before and after text "{@link " and "}"
ScriptElementKindLink ScriptElementKind = "link"
// Jsdoc @link: in `{@link C link text}`, the entity name "C"
ScriptElementKindLinkName ScriptElementKind = "link name"
// Jsdoc @link: in `{@link C link text}`, the link text "link text"
ScriptElementKindLinkText ScriptElementKind = "link text"
)
type ScriptElementKindModifier string
const (
ScriptElementKindModifierNone ScriptElementKindModifier = ""
ScriptElementKindModifierPublic ScriptElementKindModifier = "public"
ScriptElementKindModifierPrivate ScriptElementKindModifier = "private"
ScriptElementKindModifierProtected ScriptElementKindModifier = "protected"
ScriptElementKindModifierExported ScriptElementKindModifier = "export"
ScriptElementKindModifierAmbient ScriptElementKindModifier = "declare"
ScriptElementKindModifierStatic ScriptElementKindModifier = "static"
ScriptElementKindModifierAbstract ScriptElementKindModifier = "abstract"
ScriptElementKindModifierOptional ScriptElementKindModifier = "optional"
ScriptElementKindModifierDeprecated ScriptElementKindModifier = "deprecated"
ScriptElementKindModifierDts ScriptElementKindModifier = ".d.ts"
ScriptElementKindModifierTs ScriptElementKindModifier = ".ts"
ScriptElementKindModifierTsx ScriptElementKindModifier = ".tsx"
ScriptElementKindModifierJs ScriptElementKindModifier = ".js"
ScriptElementKindModifierJsx ScriptElementKindModifier = ".jsx"
ScriptElementKindModifierJson ScriptElementKindModifier = ".json"
ScriptElementKindModifierDmts ScriptElementKindModifier = ".d.mts"
ScriptElementKindModifierMts ScriptElementKindModifier = ".mts"
ScriptElementKindModifierMjs ScriptElementKindModifier = ".mjs"
ScriptElementKindModifierDcts ScriptElementKindModifier = ".d.cts"
ScriptElementKindModifierCts ScriptElementKindModifier = ".cts"
ScriptElementKindModifierCjs ScriptElementKindModifier = ".cjs"
)
var fileExtensionKindModifiers = []ScriptElementKindModifier{
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 := checker.GetCombinedLocalAndExportSymbolFlags(symbol)
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 {
roots := typeChecker.GetRootSymbols(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 &&
len(typeChecker.GetCallSignatures(typeChecker.GetNonNullableType(typeChecker.GetTypeOfSymbolAtLocation(symbol, location)))) > 0 {
return ScriptElementKindMemberFunctionElement
}
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 := checker.GetCombinedLocalAndExportSymbolFlags(symbol)
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 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.IsParameter(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) collections.Set[ScriptElementKindModifier] {
if symbol == nil {
return collections.Set[ScriptElementKindModifier]{}
}
modifiers := getNormalizedSymbolModifiers(typeChecker, symbol)
if symbol.Flags&ast.SymbolFlagsAlias != 0 {
resolvedSymbol := typeChecker.GetAliasedSymbol(symbol)
if resolvedSymbol != symbol {
aliasModifiers := getNormalizedSymbolModifiers(typeChecker, resolvedSymbol)
for modifier := range aliasModifiers.Keys() {
modifiers.Add(modifier)
}
}
}
if symbol.Flags&ast.SymbolFlagsOptional != 0 {
modifiers.Add(ScriptElementKindModifierOptional)
}
return modifiers
}
func getNormalizedSymbolModifiers(typeChecker *checker.Checker, symbol *ast.Symbol) collections.Set[ScriptElementKindModifier] {
var modifierSet collections.Set[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 &&
typeChecker.IsDeprecatedDeclaration(declaration) && // !!! include jsdoc node flags
core.Some(declarations, func(d *ast.Node) bool { return !typeChecker.IsDeprecatedDeclaration(d) }) {
excludeFlags = ast.ModifierFlagsDeprecated
} else {
excludeFlags = ast.ModifierFlagsNone
}
modifierSet = getNodeModifiers(declaration, excludeFlags)
}
return modifierSet
}
func getNodeModifiers(node *ast.Node, excludeFlags ast.ModifierFlags) collections.Set[ScriptElementKindModifier] {
var result collections.Set[ScriptElementKindModifier]
var flags ast.ModifierFlags
if ast.IsDeclaration(node) {
flags = ast.GetCombinedModifierFlags(node) & ^excludeFlags // !!! include jsdoc node flags
}
if flags&ast.ModifierFlagsPrivate != 0 {
result.Add(ScriptElementKindModifierPrivate)
}
if flags&ast.ModifierFlagsProtected != 0 {
result.Add(ScriptElementKindModifierProtected)
}
if flags&ast.ModifierFlagsPublic != 0 {
result.Add(ScriptElementKindModifierPublic)
}
if flags&ast.ModifierFlagsStatic != 0 {
result.Add(ScriptElementKindModifierStatic)
}
if flags&ast.ModifierFlagsAbstract != 0 {
result.Add(ScriptElementKindModifierAbstract)
}
if flags&ast.ModifierFlagsExport != 0 {
result.Add(ScriptElementKindModifierExported)
}
if flags&ast.ModifierFlagsDeprecated != 0 {
result.Add(ScriptElementKindModifierDeprecated)
}
if flags&ast.ModifierFlagsAmbient != 0 {
result.Add(ScriptElementKindModifierAmbient)
}
if node.Flags&ast.NodeFlagsAmbient != 0 {
result.Add(ScriptElementKindModifierAmbient)
}
if node.Kind == ast.KindExportAssignment {
result.Add(ScriptElementKindModifierExported)
}
return result
}

View File

@ -1,309 +0,0 @@
package ls
import (
"context"
"slices"
"strings"
"unicode"
"unicode/utf8"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/printer"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/scanner"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
)
func (l *LanguageService) ProvideDocumentSymbols(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.DocumentSymbolResponse, error) {
_, file := l.getProgramAndFile(documentURI)
symbols := l.getDocumentSymbolsForChildren(ctx, file.AsNode())
return lsproto.SymbolInformationsOrDocumentSymbolsOrNull{DocumentSymbols: &symbols}, nil
}
func (l *LanguageService) getDocumentSymbolsForChildren(ctx context.Context, node *ast.Node) []*lsproto.DocumentSymbol {
var symbols []*lsproto.DocumentSymbol
addSymbolForNode := func(node *ast.Node, children []*lsproto.DocumentSymbol) {
if node.Flags&ast.NodeFlagsReparsed == 0 {
symbol := l.newDocumentSymbol(node, 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 {
saveSymbols := symbols
symbols = nil
node.ForEachChild(visit)
result = symbols
symbols = saveSymbols
}
return result
}
visit = func(node *ast.Node) bool {
if ctx.Err() != nil {
return true
}
switch node.Kind {
case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration:
addSymbolForNode(node, getSymbolsForChildren(node))
case ast.KindModuleDeclaration:
addSymbolForNode(node, getSymbolsForChildren(getInteriorModule(node)))
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindMethodDeclaration, ast.KindGetAccessor,
ast.KindSetAccessor, ast.KindConstructor:
addSymbolForNode(node, getSymbolsForChildren(node.Body()))
case ast.KindVariableDeclaration, ast.KindBindingElement, ast.KindPropertyAssignment, ast.KindPropertyDeclaration:
name := node.Name()
if name != nil {
if ast.IsBindingPattern(name) {
visit(name)
} else {
addSymbolForNode(node, getSymbolsForChildren(node.Initializer()))
}
}
case ast.KindMethodSignature, ast.KindPropertySignature, ast.KindCallSignature, ast.KindConstructSignature, ast.KindIndexSignature,
ast.KindEnumMember, ast.KindShorthandPropertyAssignment, ast.KindTypeAliasDeclaration:
addSymbolForNode(node, nil)
default:
node.ForEachChild(visit)
}
return false
}
node.ForEachChild(visit)
return symbols
}
func (l *LanguageService) newDocumentSymbol(node *ast.Node, children []*lsproto.DocumentSymbol) *lsproto.DocumentSymbol {
result := new(lsproto.DocumentSymbol)
file := ast.GetSourceFileOfNode(node)
nodeStartPos := scanner.SkipTrivia(file.Text(), node.Pos())
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 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
}
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
}
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 {
switch node.Kind {
case ast.KindFunctionExpression, ast.KindArrowFunction:
if ast.IsCallExpression(node.Parent) {
name := getCallExpressionName(node.Parent.Expression())
if name != "" {
return name + "() callback"
}
}
return "<function>"
case ast.KindClassExpression:
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 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 *Converters, query string) (lsproto.WorkspaceSymbolResponse, error) {
// Obtain set of non-declaration source files from all active programs.
var sourceFiles collections.Set[*ast.SourceFile]
for _, program := range programs {
for _, sourceFile := range program.SourceFiles() {
if !sourceFile.IsDeclarationFile {
sourceFiles.Add(sourceFile)
}
}
}
// Create DeclarationInfos for all declarations in the source files.
var infos []DeclarationInfo
for sourceFile := range sourceFiles.Keys() {
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 := core.OrElse(ast.GetNameOfDeclaration(info.declaration), info.declaration)
sourceFile := ast.GetSourceFileOfNode(node)
pos := scanner.SkipTrivia(sourceFile.Text(), node.Pos())
var symbol lsproto.SymbolInformation
symbol.Name = info.name
symbol.Kind = getSymbolKindFromNode(info.declaration)
symbol.Location = converters.ToLSPLocation(sourceFile, core.NewTextRange(pos, node.End()))
symbols[i] = &symbol
}
return lsproto.SymbolInformationsOrWorkspaceSymbolsOrNull{SymbolInformations: &symbols}, nil
}
// 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()
}
func getSymbolKindFromNode(node *ast.Node) lsproto.SymbolKind {
switch node.Kind {
case ast.KindModuleDeclaration:
return lsproto.SymbolKindNamespace
case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindTypeAliasDeclaration:
return lsproto.SymbolKindClass
case ast.KindMethodDeclaration, ast.KindMethodSignature:
return lsproto.SymbolKindMethod
case ast.KindPropertyDeclaration, ast.KindPropertySignature, ast.KindGetAccessor, ast.KindSetAccessor:
return lsproto.SymbolKindProperty
case ast.KindConstructor, ast.KindConstructSignature:
return lsproto.SymbolKindConstructor
case ast.KindEnumDeclaration:
return lsproto.SymbolKindEnum
case ast.KindInterfaceDeclaration:
return lsproto.SymbolKindInterface
case ast.KindFunctionDeclaration, ast.KindFunctionExpression:
return lsproto.SymbolKindFunction
case ast.KindEnumMember:
return lsproto.SymbolKindEnumMember
case ast.KindTypeParameter:
return lsproto.SymbolKindTypeParameter
}
return lsproto.SymbolKindVariable
}

View File

@ -1,211 +0,0 @@
package ls
import (
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/modulespecifiers"
)
type UserPreferences struct {
QuotePreference QuotePreference
LazyConfiguredProjectsFromExternalProject bool // !!!
// A positive integer indicating the maximum length of a hover text before it is truncated.
//
// Default: `500`
MaximumHoverLength int // !!!
// ------- 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
// 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
// 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
// Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`.
IncludeCompletionsWithSnippetText core.Tristate // !!!
// 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 // !!!
// 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 // !!!
JsxAttributeCompletionStyle JsxAttributeCompletionStyle
// ------- AutoImports --------
ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference // !!!
// Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js"
ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference // !!!
IncludePackageJsonAutoImports IncludePackageJsonAutoImports // !!!
AutoImportSpecifierExcludeRegexes []string // !!!
AutoImportFileExcludePatterns []string // !!!
PreferTypeOnlyAutoImports bool // !!!
// ------- OrganizeImports -------
// Indicates whether imports should be organized in a case-insensitive manner.
//
// Default: TSUnknown ("auto" in strada), will perform detection
OrganizeImportsIgnoreCase core.Tristate // !!!
// Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value of their
// code points, or via "unicode" collation (via the Unicode Collation Algorithm (https://unicode.org/reports/tr10/#Scope))
//
// using rules associated with the locale specified in organizeImportsCollationLocale.
//
// Default: Ordinal
OrganizeImportsCollation OrganizeImportsCollation // !!!
// Indicates the locale to use for "unicode" collation. If not specified, the locale `"en"` is used as an invariant
// for the sake of consistent sorting. Use `"auto"` to use the detected UI locale.
//
// This preference is ignored if organizeImportsCollation is not `unicode`.
//
// Default: `"en"`
OrganizeImportsLocale string // !!!
// 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 bool // !!!
// Indicates whether accents and other diacritic marks are considered unequal for the purpose of collation. When
// `true`, characters with accents and other diacritics will be collated in the order defined by the locale specified
// in organizeImportsCollationLocale.
//
// This preference is ignored if organizeImportsCollation is not `unicode`.
//
// Default: `true`
OrganizeImportsAccentCollation OrganizeImportsAccentCollation // !!!
// Indicates whether upper case or lower case should sort first. When `false`, the default order for the locale
// specified in organizeImportsCollationLocale is used.
//
// This preference 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 // !!!
// Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is type-only.
//
// Default: `last`
OrganizeImportsTypeOrder OrganizeImportsTypeOrder // !!!
// ------- MoveToFile -------
AllowTextChangesInNewFiles bool // !!!
// ------- Rename -------
// renamed from `providePrefixAndSuffixTextForRename`
UseAliasesForRename core.Tristate
AllowRenameOfImportPath bool // !!!
// ------- CodeFixes/Refactors -------
ProvideRefactorNotApplicableReason bool // !!!
// ------- InlayHints -------
IncludeInlayParameterNameHints IncludeInlayParameterNameHints
IncludeInlayParameterNameHintsWhenArgumentMatchesName bool
IncludeInlayFunctionParameterTypeHints bool
IncludeInlayVariableTypeHints bool
IncludeInlayVariableTypeHintsWhenTypeMatchesName bool
IncludeInlayPropertyDeclarationTypeHints bool
IncludeInlayFunctionLikeReturnTypeHints bool
IncludeInlayEnumMemberValueHints bool
InteractiveInlayHints bool
// ------- Misc -------
ExcludeLibrarySymbolsInNavTo bool // !!!
DisableSuggestions bool // !!!
DisableLineTextInReferences bool // !!!
DisplayPartsForJSDoc bool // !!!
}
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 IncludePackageJsonAutoImports string
const (
IncludePackageJsonAutoImportsUnknown IncludePackageJsonAutoImports = ""
IncludePackageJsonAutoImportsAuto IncludePackageJsonAutoImports = "auto"
IncludePackageJsonAutoImportsOn IncludePackageJsonAutoImports = "on"
IncludePackageJsonAutoImportsOff IncludePackageJsonAutoImports = "off"
)
type OrganizeImportsCollation bool
const (
OrganizeImportsCollationOrdinal OrganizeImportsCollation = false
OrganizeImportsCollationUnicode OrganizeImportsCollation = true
)
type OrganizeImportsAccentCollation int
const (
OrganizeImportsAccentCollationTrue OrganizeImportsAccentCollation = 0
OrganizeImportsAccentCollationFalse OrganizeImportsAccentCollation = 1
)
type OrganizeImportsCaseFirst int
const (
OrganizeImportsCaseFirstFalse OrganizeImportsCaseFirst = 0
OrganizeImportsCaseFirstLower OrganizeImportsCaseFirst = 1
OrganizeImportsCaseFirstUpper OrganizeImportsCaseFirst = 2
)
type OrganizeImportsTypeOrder int
const (
OrganizeImportsTypeOrderLast OrganizeImportsTypeOrder = 0
OrganizeImportsTypeOrderInline OrganizeImportsTypeOrder = 1
OrganizeImportsTypeOrderFirst OrganizeImportsTypeOrder = 2
)
type QuotePreference string
const (
QuotePreferenceUnknown QuotePreference = ""
QuotePreferenceAuto QuotePreference = "auto"
QuotePreferenceDouble QuotePreference = "double"
QuotePreferenceSingle QuotePreference = "single"
)
func (p *UserPreferences) Parse(config map[string]interface{}) {
}
func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences {
return modulespecifiers.UserPreferences{
ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference,
ImportModuleSpecifierEnding: p.ImportModuleSpecifierEnding,
AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes,
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,104 +0,0 @@
package lsutil
import (
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/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.GetECMALineAndCharacterOfPosition(file, node.End())
endLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, astnav.GetStartOfNode(nextToken, file, false /*includeJSDoc*/))
return startLine != endLine
}

View File

@ -1,130 +0,0 @@
package lsutil
import (
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/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)
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 || node.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)
}
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.")
}
}