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 == "" || name == "" || 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 "" case ast.KindClassDeclaration, ast.KindClassExpression: if node.ModifierFlags()&ast.ModifierFlagsDefault != 0 { return "default" } return "" 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 }