695 lines
24 KiB
Go
695 lines
24 KiB
Go
package ls
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/microsoft/typescript-go/internal/ast"
|
|
"github.com/microsoft/typescript-go/internal/astnav"
|
|
"github.com/microsoft/typescript-go/internal/collections"
|
|
"github.com/microsoft/typescript-go/internal/compiler"
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
|
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
|
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
|
"github.com/microsoft/typescript-go/internal/printer"
|
|
"github.com/microsoft/typescript-go/internal/scanner"
|
|
"github.com/microsoft/typescript-go/internal/stringutil"
|
|
"github.com/microsoft/typescript-go/internal/tspath"
|
|
)
|
|
|
|
func (l *LanguageService) ProvideDocumentSymbols(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.DocumentSymbolResponse, error) {
|
|
_, file := l.getProgramAndFile(documentURI)
|
|
if lsproto.GetClientCapabilities(ctx).TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport {
|
|
symbols := l.getDocumentSymbolsForChildren(ctx, file.AsNode(), file)
|
|
return lsproto.SymbolInformationsOrDocumentSymbolsOrNull{DocumentSymbols: &symbols}, nil
|
|
}
|
|
// Client doesn't support hierarchical document symbols, return flat SymbolInformation array
|
|
symbolInfos := l.getDocumentSymbolInformations(ctx, file, documentURI)
|
|
symbolInfoPtrs := make([]*lsproto.SymbolInformation, len(symbolInfos))
|
|
for i := range symbolInfos {
|
|
symbolInfoPtrs[i] = &symbolInfos[i]
|
|
}
|
|
return lsproto.SymbolInformationsOrDocumentSymbolsOrNull{SymbolInformations: &symbolInfoPtrs}, nil
|
|
}
|
|
|
|
// getDocumentSymbolInformations converts hierarchical DocumentSymbols to a flat SymbolInformation array
|
|
func (l *LanguageService) getDocumentSymbolInformations(ctx context.Context, file *ast.SourceFile, documentURI lsproto.DocumentUri) []lsproto.SymbolInformation {
|
|
// First get hierarchical symbols
|
|
docSymbols := l.getDocumentSymbolsForChildren(ctx, file.AsNode(), file)
|
|
|
|
// Flatten the hierarchy
|
|
var result []lsproto.SymbolInformation
|
|
var flatten func(symbols []*lsproto.DocumentSymbol, containerName *string)
|
|
flatten = func(symbols []*lsproto.DocumentSymbol, containerName *string) {
|
|
for _, symbol := range symbols {
|
|
info := lsproto.SymbolInformation{
|
|
Name: symbol.Name,
|
|
Kind: symbol.Kind,
|
|
Location: lsproto.Location{
|
|
Uri: documentURI,
|
|
Range: symbol.Range,
|
|
},
|
|
ContainerName: containerName,
|
|
Tags: symbol.Tags,
|
|
Deprecated: symbol.Deprecated,
|
|
}
|
|
result = append(result, info)
|
|
|
|
// Recursively flatten children with this symbol as container
|
|
if symbol.Children != nil && len(*symbol.Children) > 0 {
|
|
flatten(*symbol.Children, &symbol.Name)
|
|
}
|
|
}
|
|
}
|
|
flatten(docSymbols, nil)
|
|
return result
|
|
}
|
|
|
|
func (l *LanguageService) getDocumentSymbolsForChildren(ctx context.Context, node *ast.Node, file *ast.SourceFile) []*lsproto.DocumentSymbol {
|
|
var symbols []*lsproto.DocumentSymbol
|
|
expandoTargets := collections.Set[string]{}
|
|
addSymbolForNode := func(node *ast.Node, name *ast.Node, children []*lsproto.DocumentSymbol) {
|
|
if node.Flags&ast.NodeFlagsReparsed == 0 {
|
|
symbol := l.newDocumentSymbol(node, name, children)
|
|
if symbol != nil {
|
|
symbols = append(symbols, symbol)
|
|
}
|
|
}
|
|
}
|
|
var visit func(*ast.Node) bool
|
|
getSymbolsForChildren := func(node *ast.Node) []*lsproto.DocumentSymbol {
|
|
var result []*lsproto.DocumentSymbol
|
|
if node != nil {
|
|
saveExpandoTargets := expandoTargets
|
|
expandoTargets = collections.Set[string]{}
|
|
saveSymbols := symbols
|
|
symbols = nil
|
|
node.ForEachChild(visit)
|
|
result = symbols
|
|
symbols = saveSymbols
|
|
expandoTargets = saveExpandoTargets
|
|
}
|
|
return result
|
|
}
|
|
startNode := func(node *ast.Node, name *ast.Node) func() {
|
|
if node == nil {
|
|
return func() {}
|
|
}
|
|
saveExpandoTargets := expandoTargets
|
|
expandoTargets = collections.Set[string]{}
|
|
saveSymbols := symbols
|
|
symbols = nil
|
|
return func() {
|
|
result := symbols
|
|
symbols = saveSymbols
|
|
expandoTargets = saveExpandoTargets
|
|
addSymbolForNode(node, name, result)
|
|
}
|
|
}
|
|
getSymbolsForNode := func(node *ast.Node) []*lsproto.DocumentSymbol {
|
|
var result []*lsproto.DocumentSymbol
|
|
if node != nil {
|
|
saveSymbols := symbols
|
|
symbols = nil
|
|
visit(node)
|
|
result = symbols
|
|
symbols = saveSymbols
|
|
}
|
|
return result
|
|
}
|
|
visit = func(node *ast.Node) bool {
|
|
if ctx.Err() != nil {
|
|
return true
|
|
}
|
|
if node.Flags&ast.NodeFlagsReparsed == 0 {
|
|
if jsdocs := node.JSDoc(file); len(jsdocs) > 0 {
|
|
for _, jsdoc := range jsdocs {
|
|
if tagList := jsdoc.AsJSDoc().Tags; tagList != nil {
|
|
for _, tag := range tagList.Nodes {
|
|
if ast.IsJSDocTypedefTag(tag) || ast.IsJSDocCallbackTag(tag) {
|
|
addSymbolForNode(tag, nil /*name*/, nil /*children*/)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
switch node.Kind {
|
|
case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration:
|
|
if ast.IsClassLike(node) && ast.GetDeclarationName(node) != "" {
|
|
expandoTargets.Add(ast.GetDeclarationName(node))
|
|
}
|
|
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node))
|
|
case ast.KindModuleDeclaration:
|
|
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(getInteriorModule(node)))
|
|
case ast.KindConstructor:
|
|
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node.Body()))
|
|
for _, param := range node.Parameters() {
|
|
if ast.IsParameterPropertyDeclaration(param, node) {
|
|
addSymbolForNode(param, nil /*name*/, nil /*children*/)
|
|
}
|
|
}
|
|
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction, ast.KindMethodDeclaration, ast.KindGetAccessor,
|
|
ast.KindSetAccessor:
|
|
declName := ast.GetDeclarationName(node)
|
|
if declName != "" {
|
|
expandoTargets.Add(declName)
|
|
}
|
|
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node.Body()))
|
|
case ast.KindVariableDeclaration, ast.KindBindingElement, ast.KindPropertyAssignment, ast.KindPropertyDeclaration:
|
|
nodeName := node.Name()
|
|
if nodeName != nil {
|
|
if ast.IsBindingPattern(nodeName) {
|
|
visit(nodeName)
|
|
} else {
|
|
addSymbolForNode(node, nil /*name*/, getSymbolsForChildren(node.Initializer()))
|
|
}
|
|
}
|
|
case ast.KindSpreadAssignment:
|
|
addSymbolForNode(node, node.Expression(), nil /*children*/)
|
|
case ast.KindMethodSignature, ast.KindPropertySignature, ast.KindCallSignature, ast.KindConstructSignature, ast.KindIndexSignature,
|
|
ast.KindEnumMember, ast.KindShorthandPropertyAssignment, ast.KindTypeAliasDeclaration, ast.KindImportEqualsDeclaration, ast.KindExportSpecifier:
|
|
addSymbolForNode(node, nil /*name*/, nil /*children*/)
|
|
case ast.KindImportClause:
|
|
// Handle default import case e.g.:
|
|
// import d from "mod";
|
|
if node.Name() != nil {
|
|
addSymbolForNode(node.Name(), node.Name(), nil /*children*/)
|
|
}
|
|
// Handle named bindings in imports e.g.:
|
|
// import * as NS from "mod";
|
|
// import {a, b as B} from "mod";
|
|
if namedBindings := node.AsImportClause().NamedBindings; namedBindings != nil {
|
|
if namedBindings.Kind == ast.KindNamespaceImport {
|
|
addSymbolForNode(namedBindings, nil /*name*/, nil /*children*/)
|
|
} else {
|
|
for _, element := range namedBindings.Elements() {
|
|
addSymbolForNode(element, nil /*name*/, nil /*children*/)
|
|
}
|
|
}
|
|
}
|
|
case ast.KindBinaryExpression, ast.KindCallExpression:
|
|
assignmentKind := ast.GetAssignmentDeclarationKind(node)
|
|
switch assignmentKind {
|
|
// `module.exports = ...`` should be reparsed into a JSExportAssignment,
|
|
// and `exports.a = ...`` into a CommonJSExport.
|
|
case ast.JSDeclarationKindNone, ast.JSDeclarationKindThisProperty,
|
|
ast.JSDeclarationKindModuleExports, ast.JSDeclarationKindExportsProperty,
|
|
ast.JSDeclarationKindObjectDefinePropertyExports:
|
|
node.ForEachChild(visit)
|
|
case ast.JSDeclarationKindProperty, ast.JSDeclarationKindObjectDefinePropertyValue:
|
|
var target *ast.Expression
|
|
var targetFunction *ast.Expression
|
|
var definition *ast.Node
|
|
var propertyName *ast.Node
|
|
// `A.b = ... ` or `A.prototype.b = ...`
|
|
if ast.IsBinaryExpression(node) {
|
|
binaryExpr := node.AsBinaryExpression()
|
|
target = binaryExpr.Left
|
|
targetFunction = target.Expression()
|
|
definition = binaryExpr.Right
|
|
// `A.b` or `A.prototype.b`
|
|
if ast.IsPropertyAccessExpression(target) {
|
|
propertyName = target.AsPropertyAccessExpression().Name()
|
|
} else { // `A["b"]` or `A.prototype["b"]`
|
|
propertyName = target.AsElementAccessExpression().ArgumentExpression
|
|
}
|
|
} else { // `Object.defineProperty(A, "b", {...})`
|
|
args := node.Arguments()
|
|
targetFunction = args[0]
|
|
target = args[1]
|
|
propertyName = target
|
|
definition = args[2]
|
|
}
|
|
if isPrototypeExpando(targetFunction) {
|
|
targetFunction = targetFunction.Expression()
|
|
// If we see a prototype assignment, start tracking the target as an expando target.
|
|
if ast.IsIdentifier(targetFunction) {
|
|
expandoTargets.Add(targetFunction.Text())
|
|
}
|
|
}
|
|
if ast.IsIdentifier(targetFunction) &&
|
|
expandoTargets.Has(targetFunction.Text()) {
|
|
endNode := startNode(node, targetFunction)
|
|
addSymbolForNode(target, propertyName, getSymbolsForNode(definition))
|
|
endNode()
|
|
} else {
|
|
node.ForEachChild(visit)
|
|
}
|
|
}
|
|
case ast.KindExportAssignment:
|
|
if node.AsExportAssignment().IsExportEquals {
|
|
addSymbolForNode(node, nil /*name*/, getSymbolsForNode(node.Expression()))
|
|
} else {
|
|
node.ForEachChild(visit)
|
|
}
|
|
default:
|
|
node.ForEachChild(visit)
|
|
}
|
|
return false
|
|
}
|
|
node.ForEachChild(visit)
|
|
return mergeExpandos(symbols)
|
|
}
|
|
|
|
// Target is `f.prototype`.
|
|
func isPrototypeExpando(target *ast.Node) bool {
|
|
if ast.IsAccessExpression(target) {
|
|
accessName := ast.GetElementOrPropertyAccessName(target)
|
|
return accessName != nil && accessName.Text() == "prototype"
|
|
}
|
|
return false
|
|
}
|
|
|
|
const maxLength = 150
|
|
|
|
func (l *LanguageService) newDocumentSymbol(node *ast.Node, name *ast.Node, children []*lsproto.DocumentSymbol) *lsproto.DocumentSymbol {
|
|
result := new(lsproto.DocumentSymbol)
|
|
file := ast.GetSourceFileOfNode(node)
|
|
nodeStartPos := scanner.SkipTrivia(file.Text(), node.Pos())
|
|
if name == nil {
|
|
name = ast.GetNameOfDeclaration(node)
|
|
}
|
|
var text string
|
|
var nameStartPos, nameEndPos int
|
|
if ast.IsModuleDeclaration(node) && !ast.IsAmbientModule(node) {
|
|
text = getModuleName(node)
|
|
nameStartPos = scanner.SkipTrivia(file.Text(), name.Pos())
|
|
nameEndPos = getInteriorModule(node).Name().End()
|
|
} else if ast.IsAnyExportAssignment(node) && node.AsExportAssignment().IsExportEquals {
|
|
text = "export="
|
|
if !ast.NodeIsMissing(name) {
|
|
nameStartPos = scanner.SkipTrivia(file.Text(), name.Pos())
|
|
nameEndPos = name.End()
|
|
} else {
|
|
nameStartPos = nodeStartPos
|
|
nameEndPos = node.End()
|
|
}
|
|
} else if name != nil {
|
|
text = getTextOfName(name)
|
|
nameStartPos = max(scanner.SkipTrivia(file.Text(), name.Pos()), nodeStartPos)
|
|
nameEndPos = max(name.End(), nodeStartPos)
|
|
} else {
|
|
text = getUnnamedNodeLabel(node)
|
|
nameStartPos = nodeStartPos
|
|
nameEndPos = nodeStartPos
|
|
}
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
truncatedText := stringutil.TruncateByRunes(text, maxLength)
|
|
if len(truncatedText) < len(text) {
|
|
text = truncatedText + "..."
|
|
}
|
|
result.Name = text
|
|
result.Kind = getSymbolKindFromNode(node)
|
|
result.Range = lsproto.Range{
|
|
Start: l.converters.PositionToLineAndCharacter(file, core.TextPos(nodeStartPos)),
|
|
End: l.converters.PositionToLineAndCharacter(file, core.TextPos(node.End())),
|
|
}
|
|
result.SelectionRange = lsproto.Range{
|
|
Start: l.converters.PositionToLineAndCharacter(file, core.TextPos(nameStartPos)),
|
|
End: l.converters.PositionToLineAndCharacter(file, core.TextPos(nameEndPos)),
|
|
}
|
|
if children == nil {
|
|
children = []*lsproto.DocumentSymbol{}
|
|
}
|
|
result.Children = &children
|
|
return result
|
|
}
|
|
|
|
// Merges expando symbols into their target symbols, and namespaces of same name.
|
|
// Modifies the input slice.
|
|
func mergeExpandos(symbols []*lsproto.DocumentSymbol) []*lsproto.DocumentSymbol {
|
|
mergedSymbols := make([]*lsproto.DocumentSymbol, 0, len(symbols))
|
|
// Collect symbols that can be an expando target.
|
|
nameToExpandoTargetIndex := collections.MultiMap[string, int]{}
|
|
// Collect namespaces.
|
|
nameToNamespaceIndex := map[string]int{}
|
|
for i, symbol := range symbols {
|
|
if isAnonymousName(symbol.Name) {
|
|
continue
|
|
}
|
|
if symbol.Kind == lsproto.SymbolKindClass || symbol.Kind == lsproto.SymbolKindFunction || symbol.Kind == lsproto.SymbolKindVariable {
|
|
nameToExpandoTargetIndex.Add(symbol.Name, i)
|
|
}
|
|
if symbol.Kind == lsproto.SymbolKindNamespace {
|
|
if _, ok := nameToNamespaceIndex[symbol.Name]; !ok {
|
|
nameToNamespaceIndex[symbol.Name] = i
|
|
}
|
|
}
|
|
}
|
|
for i, symbol := range symbols {
|
|
if symbol.Children != nil {
|
|
children := mergeExpandos(*symbol.Children)
|
|
symbol.Children = &children
|
|
}
|
|
|
|
// Anonymous symbols never merge.
|
|
if isAnonymousName(symbol.Name) {
|
|
continue
|
|
}
|
|
|
|
// Merge expandos.
|
|
if symbol.Kind == lsproto.SymbolKindProperty {
|
|
symbolsWithSameName := nameToExpandoTargetIndex.Get(symbol.Name)
|
|
for j := len(symbolsWithSameName) - 1; j >= 0; j-- {
|
|
targetIndex := symbolsWithSameName[j]
|
|
targetSymbol := symbols[targetIndex]
|
|
mergeChildren(targetSymbol, symbol)
|
|
// Mark this symbol as merged.
|
|
symbols[i] = nil
|
|
}
|
|
}
|
|
// Merge namespaces.
|
|
if symbol.Kind == lsproto.SymbolKindNamespace {
|
|
if targetIndex, ok := nameToNamespaceIndex[symbol.Name]; ok && targetIndex != i {
|
|
targetSymbol := symbols[targetIndex]
|
|
mergeChildren(targetSymbol, symbol)
|
|
// Mark this symbol as merged.
|
|
symbols[i] = nil
|
|
}
|
|
}
|
|
}
|
|
for _, symbol := range symbols {
|
|
if symbol != nil {
|
|
mergedSymbols = append(mergedSymbols, symbol)
|
|
}
|
|
}
|
|
return mergedSymbols
|
|
}
|
|
|
|
func mergeChildren(target *lsproto.DocumentSymbol, source *lsproto.DocumentSymbol) {
|
|
if source.Children != nil {
|
|
if target.Children == nil {
|
|
target.Children = source.Children
|
|
} else {
|
|
*target.Children = mergeExpandos(append(*target.Children, *source.Children...))
|
|
slices.SortFunc(*target.Children, func(a, b *lsproto.DocumentSymbol) int {
|
|
return lsproto.CompareRanges(a.Range, b.Range)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// See `getUnnamedNodeLabel`.
|
|
func isAnonymousName(name string) bool {
|
|
return name == "<function>" || name == "<class>" || name == "export=" || name == "default" ||
|
|
name == "constructor" || name == "()" || name == "new()" || name == "[]" || strings.HasSuffix(name, ") callback")
|
|
}
|
|
|
|
func getTextOfName(node *ast.Node) string {
|
|
switch node.Kind {
|
|
case ast.KindIdentifier, ast.KindPrivateIdentifier, ast.KindNumericLiteral:
|
|
return node.Text()
|
|
case ast.KindStringLiteral:
|
|
return "\"" + printer.EscapeString(node.Text(), '"') + "\""
|
|
case ast.KindNoSubstitutionTemplateLiteral:
|
|
return "`" + printer.EscapeString(node.Text(), '`') + "`"
|
|
case ast.KindComputedPropertyName:
|
|
if ast.IsStringOrNumericLiteralLike(node.Expression()) {
|
|
return getTextOfName(node.Expression())
|
|
}
|
|
}
|
|
return scanner.GetTextOfNode(node)
|
|
}
|
|
|
|
func getUnnamedNodeLabel(node *ast.Node) string {
|
|
if parent := ast.WalkUpParenthesizedExpressions(node.Parent); parent != nil && ast.IsExportAssignment(parent) {
|
|
if parent.AsExportAssignment().IsExportEquals {
|
|
return "export="
|
|
}
|
|
return "default"
|
|
}
|
|
switch node.Kind {
|
|
case ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindArrowFunction:
|
|
if node.ModifierFlags()&ast.ModifierFlagsDefault != 0 {
|
|
return "default"
|
|
}
|
|
if ast.IsCallExpression(node.Parent) {
|
|
name := getCallExpressionName(node.Parent.Expression())
|
|
if name != "" {
|
|
name = cleanCallbackText(name)
|
|
if len(name) > maxLength {
|
|
return name + " callback"
|
|
}
|
|
args := cleanCallbackText(getCallExpressionLiteralArgs(node.Parent))
|
|
return name + "(" + args + ") callback"
|
|
}
|
|
}
|
|
return "<function>"
|
|
case ast.KindClassDeclaration, ast.KindClassExpression:
|
|
if node.ModifierFlags()&ast.ModifierFlagsDefault != 0 {
|
|
return "default"
|
|
}
|
|
return "<class>"
|
|
case ast.KindConstructor:
|
|
return "constructor"
|
|
case ast.KindCallSignature:
|
|
return "()"
|
|
case ast.KindConstructSignature:
|
|
return "new()"
|
|
case ast.KindIndexSignature:
|
|
return "[]"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getCallExpressionName(node *ast.Node) string {
|
|
switch node.Kind {
|
|
case ast.KindIdentifier, ast.KindPrivateIdentifier:
|
|
return node.Text()
|
|
case ast.KindPropertyAccessExpression:
|
|
left := getCallExpressionName(node.Expression())
|
|
right := getCallExpressionName(node.Name())
|
|
if left != "" {
|
|
return left + "." + right
|
|
}
|
|
return right
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getCallExpressionLiteralArgs(callExpr *ast.Node) string {
|
|
var parts []string
|
|
for _, arg := range callExpr.Arguments() {
|
|
if ast.IsStringLiteralLike(arg) || ast.IsTemplateExpression(arg) {
|
|
parts = append(parts, scanner.GetTextOfNode(arg))
|
|
}
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
func cleanCallbackText(text string) string {
|
|
truncated := stringutil.TruncateByRunes(text, maxLength)
|
|
if len(truncated) < len(text) {
|
|
text = truncated + "..."
|
|
}
|
|
return strings.Map(func(r rune) rune {
|
|
if stringutil.IsLineBreak(r) {
|
|
return -1
|
|
}
|
|
return r
|
|
}, text)
|
|
}
|
|
|
|
func getInteriorModule(node *ast.Node) *ast.Node {
|
|
for node.Body() != nil && ast.IsModuleDeclaration(node.Body()) {
|
|
node = node.Body()
|
|
}
|
|
return node
|
|
}
|
|
|
|
func getModuleName(node *ast.Node) string {
|
|
result := node.Name().Text()
|
|
for node.Body() != nil && ast.IsModuleDeclaration(node.Body()) {
|
|
node = node.Body()
|
|
result = result + "." + node.Name().Text()
|
|
}
|
|
return result
|
|
}
|
|
|
|
type DeclarationInfo struct {
|
|
name string
|
|
declaration *ast.Node
|
|
matchScore int
|
|
}
|
|
|
|
func ProvideWorkspaceSymbols(
|
|
ctx context.Context,
|
|
programs []*compiler.Program,
|
|
converters *lsconv.Converters,
|
|
preferences lsutil.UserPreferences,
|
|
query string,
|
|
) (lsproto.WorkspaceSymbolResponse, error) {
|
|
excludeLibrarySymbols := preferences.ExcludeLibrarySymbolsInNavTo.IsTrue()
|
|
// Obtain set of non-declaration source files from all active programs.
|
|
sourceFiles := map[tspath.Path]*ast.SourceFile{}
|
|
for _, program := range programs {
|
|
for _, sourceFile := range program.SourceFiles() {
|
|
if (program.HasTSFile() || !sourceFile.IsDeclarationFile) &&
|
|
!shouldExcludeFile(sourceFile, program, excludeLibrarySymbols) {
|
|
sourceFiles[sourceFile.Path()] = sourceFile
|
|
}
|
|
}
|
|
}
|
|
// Create DeclarationInfos for all declarations in the source files.
|
|
var infos []DeclarationInfo
|
|
for _, sourceFile := range sourceFiles {
|
|
if ctx.Err() != nil {
|
|
return lsproto.SymbolInformationsOrWorkspaceSymbolsOrNull{}, nil
|
|
}
|
|
declarationMap := sourceFile.GetDeclarationMap()
|
|
for name, declarations := range declarationMap {
|
|
score := getMatchScore(name, query)
|
|
if score >= 0 {
|
|
for _, declaration := range declarations {
|
|
infos = append(infos, DeclarationInfo{name, declaration, score})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Sort the DeclarationInfos and return the top 256 matches.
|
|
slices.SortFunc(infos, compareDeclarationInfos)
|
|
count := min(len(infos), 256)
|
|
symbols := make([]*lsproto.SymbolInformation, count)
|
|
for i, info := range infos[0:count] {
|
|
node := info.declaration
|
|
sourceFile := ast.GetSourceFileOfNode(node)
|
|
container := getContainerNode(info.declaration)
|
|
var containerName *string
|
|
if container != nil {
|
|
containerName = strPtrTo(ast.GetDeclarationName(container))
|
|
}
|
|
// Use the name node's span so that VS selects just the symbol name (matching
|
|
// the TS5 navto behaviour). GetNameOfDeclaration is always non-nil here because
|
|
// computeDeclarationMap only adds declarations whose GetDeclarationName (string
|
|
// form) is non-empty, which implies a name node exists.
|
|
nameNode := ast.GetNameOfDeclaration(node)
|
|
nameStart := astnav.GetStartOfNode(nameNode, sourceFile, false /*includeJsDoc*/)
|
|
nameRange := core.NewTextRange(nameStart, nameNode.End())
|
|
var symbol lsproto.SymbolInformation
|
|
symbol.Name = info.name
|
|
symbol.Kind = getSymbolKindFromNode(info.declaration)
|
|
symbol.Location = converters.ToLSPLocation(sourceFile, nameRange)
|
|
symbol.ContainerName = containerName
|
|
symbols[i] = &symbol
|
|
}
|
|
|
|
return lsproto.SymbolInformationsOrWorkspaceSymbolsOrNull{SymbolInformations: &symbols}, nil
|
|
}
|
|
|
|
func shouldExcludeFile(file *ast.SourceFile, program *compiler.Program, excludeLibrarySymbols bool) bool {
|
|
return excludeLibrarySymbols && (isInsideNodeModules(file.FileName()) || program.IsLibFile(file))
|
|
}
|
|
|
|
func isInsideNodeModules(fileName string) bool {
|
|
return strings.Contains(fileName, "/node_modules/")
|
|
}
|
|
|
|
// Return a score for matching `s` against `pattern`. In order to match, `s` must contain each of the characters in
|
|
// `pattern` in the same order. Upper case characters in `pattern` must match exactly, whereas lower case characters
|
|
// in `pattern` match either case in `s`. If `s` doesn't match, -1 is returned. Otherwise, the returned score is the
|
|
// number of characters in `s` that weren't matched. Thus, zero represents an exact match, and higher values represent
|
|
// increasingly less specific partial matches.
|
|
func getMatchScore(s string, pattern string) int {
|
|
score := 0
|
|
for _, p := range pattern {
|
|
exact := unicode.IsUpper(p)
|
|
for {
|
|
c, size := utf8.DecodeRuneInString(s)
|
|
if size == 0 {
|
|
return -1
|
|
}
|
|
s = s[size:]
|
|
if exact && c == p || !exact && unicode.ToLower(c) == unicode.ToLower(p) {
|
|
break
|
|
}
|
|
score++
|
|
}
|
|
}
|
|
return score
|
|
}
|
|
|
|
// Sort DeclarationInfos by ascending match score, then ascending case insensitive name, then
|
|
// ascending case sensitive name, and finally by source file name and position.
|
|
func compareDeclarationInfos(d1, d2 DeclarationInfo) int {
|
|
if d1.matchScore != d2.matchScore {
|
|
return d1.matchScore - d2.matchScore
|
|
}
|
|
if c := stringutil.CompareStringsCaseInsensitive(d1.name, d2.name); c != 0 {
|
|
return c
|
|
}
|
|
if c := strings.Compare(d1.name, d2.name); c != 0 {
|
|
return c
|
|
}
|
|
s1 := ast.GetSourceFileOfNode(d1.declaration)
|
|
s2 := ast.GetSourceFileOfNode(d2.declaration)
|
|
if s1 != s2 {
|
|
return strings.Compare(string(s1.Path()), string(s2.Path()))
|
|
}
|
|
return d1.declaration.Pos() - d2.declaration.Pos()
|
|
}
|
|
|
|
// getSymbolKindFromNode converts an AST node to an LSP SymbolKind.
|
|
// Combines getNodeKind with VS Code's fromProtocolScriptElementKind.
|
|
func getSymbolKindFromNode(node *ast.Node) lsproto.SymbolKind {
|
|
switch node.Kind {
|
|
case ast.KindSourceFile:
|
|
if ast.IsExternalModule(node.AsSourceFile()) {
|
|
return lsproto.SymbolKindModule
|
|
}
|
|
return lsproto.SymbolKindFile
|
|
case ast.KindModuleDeclaration:
|
|
return lsproto.SymbolKindNamespace
|
|
case ast.KindClassDeclaration, ast.KindClassExpression:
|
|
return lsproto.SymbolKindClass
|
|
case ast.KindInterfaceDeclaration:
|
|
return lsproto.SymbolKindInterface
|
|
case ast.KindTypeAliasDeclaration, ast.KindJSDocTypedefTag, ast.KindJSDocCallbackTag:
|
|
return lsproto.SymbolKindClass
|
|
case ast.KindEnumDeclaration:
|
|
return lsproto.SymbolKindEnum
|
|
case ast.KindVariableDeclaration:
|
|
return lsproto.SymbolKindVariable
|
|
case ast.KindArrowFunction, ast.KindFunctionDeclaration, ast.KindFunctionExpression:
|
|
return lsproto.SymbolKindFunction
|
|
case ast.KindGetAccessor, ast.KindSetAccessor:
|
|
return lsproto.SymbolKindProperty
|
|
case ast.KindMethodDeclaration, ast.KindMethodSignature:
|
|
return lsproto.SymbolKindMethod
|
|
case ast.KindPropertyDeclaration, ast.KindPropertySignature, ast.KindPropertyAssignment,
|
|
ast.KindShorthandPropertyAssignment, ast.KindSpreadAssignment, ast.KindIndexSignature:
|
|
return lsproto.SymbolKindProperty
|
|
case ast.KindCallSignature:
|
|
return lsproto.SymbolKindMethod
|
|
case ast.KindConstructSignature:
|
|
return lsproto.SymbolKindConstructor
|
|
case ast.KindConstructor, ast.KindClassStaticBlockDeclaration:
|
|
return lsproto.SymbolKindConstructor
|
|
case ast.KindTypeParameter:
|
|
return lsproto.SymbolKindTypeParameter
|
|
case ast.KindEnumMember:
|
|
return lsproto.SymbolKindEnumMember
|
|
case ast.KindParameter:
|
|
if ast.HasSyntacticModifier(node, ast.ModifierFlagsParameterPropertyModifier) {
|
|
return lsproto.SymbolKindProperty
|
|
}
|
|
return lsproto.SymbolKindVariable
|
|
case ast.KindBinaryExpression, ast.KindCallExpression:
|
|
kind := ast.GetAssignmentDeclarationKind(node)
|
|
switch kind {
|
|
case ast.JSDeclarationKindThisProperty, ast.JSDeclarationKindProperty, ast.JSDeclarationKindObjectDefinePropertyValue:
|
|
return lsproto.SymbolKindProperty
|
|
}
|
|
case ast.KindStringLiteral, ast.KindNoSubstitutionTemplateLiteral, ast.KindNumericLiteral:
|
|
// String literals used as property names (e.g., in Object.defineProperty)
|
|
return lsproto.SymbolKindProperty
|
|
}
|
|
return lsproto.SymbolKindVariable
|
|
}
|