internal/ls/symbols.go (367 lines of code) (raw):

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/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()) 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()) // 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) []*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 *lsconv.Converters, preferences *lsutil.UserPreferences, query string, ) (lsproto.WorkspaceSymbolResponse, error) { excludeLibrarySymbols := preferences.ExcludeLibrarySymbolsInNavTo // 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) pos := astnav.GetStartOfNode(node, sourceFile, false /*includeJsDoc*/) container := getContainerNode(info.declaration) var containerName *string if container != nil { containerName = strPtrTo(ast.GetDeclarationName(container)) } var symbol lsproto.SymbolInformation symbol.Name = info.name symbol.Kind = getSymbolKindFromNode(info.declaration) symbol.Location = converters.ToLSPLocation(sourceFile, core.NewTextRange(pos, node.End())) 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.SymbolKindModule case ast.KindClassDeclaration, ast.KindClassExpression: return lsproto.SymbolKindClass case ast.KindInterfaceDeclaration: return lsproto.SymbolKindInterface case ast.KindTypeAliasDeclaration: 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: return lsproto.SymbolKindProperty case ast.KindIndexSignature, 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: kind := ast.GetAssignmentDeclarationKind(node.AsBinaryExpression()) switch kind { case ast.JSDeclarationKindThisProperty, ast.JSDeclarationKindProperty: return lsproto.SymbolKindProperty } } return lsproto.SymbolKindVariable }