diff --git a/internal/indexer/indexer.go b/internal/indexer/indexer.go index 0fdfbeba..89e8b9c6 100644 --- a/internal/indexer/indexer.go +++ b/internal/indexer/indexer.go @@ -338,6 +338,16 @@ func (i *Indexer) indexDefinitionsForPackage(p *packages.Package) { } } + // Put together maps of ASTs and corresponding comments for each file in the + // package so we can pass them to the definition index function. + astFiles := make(map[string]*ast.File) + commentMaps := make(map[string]ast.CommentMap) + for index, filename := range p.GoFiles { + file := p.Syntax[index] + astFiles[filename] = file + commentMaps[filename] = ast.NewCommentMap(p.Fset, file, file.Comments) + } + for ident, obj := range p.TypesInfo.Defs { typeSwitchHeader := false if obj == nil { @@ -359,7 +369,7 @@ func (i *Indexer) indexDefinitionsForPackage(p *packages.Package) { continue } - rangeID := i.indexDefinition(p, pos.Filename, d, pos, obj, typeSwitchHeader, ident) + rangeID := i.indexDefinition(p, d, pos, obj, typeSwitchHeader, ident, astFiles[pos.Filename], commentMaps[pos.Filename]) i.stripedMutex.LockKey(pos.Filename) i.ranges[pos.Filename][pos.Offset] = rangeID @@ -411,8 +421,22 @@ func (i *Indexer) markRange(pos token.Position) bool { } // indexDefinition emits data for the given definition object. -func (i *Indexer) indexDefinition(p *packages.Package, filename string, document *DocumentInfo, pos token.Position, obj types.Object, typeSwitchHeader bool, ident *ast.Ident) uint64 { - rangeID := i.emitter.EmitRange(rangeForObject(obj, pos)) +func (i *Indexer) indexDefinition(p *packages.Package, document *DocumentInfo, pos token.Position, obj types.Object, typeSwitchHeader bool, ident *ast.Ident, file *ast.File, commentMap ast.CommentMap) uint64 { + var rangeTag *protocol.RangeTag + + start, end := rangeForObject(obj, pos) + + // Look up the AST object representing the definition. If it's not found, + // the definition isn't top-level and we're not worried about it being + // deprecated since it's not accessible outside of its scope. We could still + // emit tags for those definitions, but I'm not sure how to access the AST + // for them. + astObj := file.Scope.Lookup(ident.Name) + if astObj != nil { + rangeTag = tagForObject(p, astObj, "definition", commentMap, start, end) + } + + rangeID := i.emitter.EmitRangeWithTag(start, end, rangeTag) resultSetID := i.emitter.EmitResultSet() defResultID := i.emitter.EmitDefinitionResult() diff --git a/internal/indexer/protocol.go b/internal/indexer/protocol.go index c142695d..d88a716d 100644 --- a/internal/indexer/protocol.go +++ b/internal/indexer/protocol.go @@ -2,8 +2,10 @@ package indexer import ( "bytes" + "go/ast" "go/token" "go/types" + "golang.org/x/tools/go/packages" "strings" doc "github.com/slimsag/godocmd" @@ -12,6 +14,15 @@ import ( const languageGo = "go" +var objKindToSymbolKind = map[ast.ObjKind]protocol.SymbolKind{ + ast.Pkg: protocol.Package, + ast.Con: protocol.Constant, + ast.Typ: protocol.Class, // Based on LLVM LSP implementation; no type alias in spec + ast.Var: protocol.Variable, + ast.Fun: protocol.Function, + //ast.Lbl: protocol.:noidea: // No label in spec +} + // rangeForObject transforms the position of the given object (1-indexed) into an LSP range // (0-indexed). If the object is a quoted package name, the leading and trailing quotes are // stripped from the resulting range's bounds. @@ -30,6 +41,66 @@ func rangeForObject(obj types.Object, pos token.Position) (protocol.Pos, protoco return start, end } +func tagForObject(p *packages.Package, obj *ast.Object, rangeType string, commentMap ast.CommentMap, start, end protocol.Pos) *protocol.RangeTag { + kind, ok := objKindToSymbolKind[obj.Kind] + if !ok { + return nil + } + + deprecated := false + + // Since we're just looking at definitions right now, we assume that Decl + // will be defined or that we can kind of just ignore comments. + if obj.Decl != nil { + commentGroups := commentMap[obj.Decl.(ast.Node)] + for _, commentGroup := range commentGroups { + lines := strings.Split(commentGroup.Text(), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "Deprecated: ") { + deprecated = true + break + } + } + + // The tag range wants us to include comments, so we adjust if the + // comments fall outside the current range. + commentStart := p.Fset.Position(commentGroup.Pos()) + startLine := commentStart.Line - 1 + startColumn := commentStart.Column - 1 + if startLine < start.Line || (startLine == start.Line && startColumn < start.Character) { + start.Line = startLine + start.Character = startColumn + } + + commentEnd := p.Fset.Position(commentGroup.End()) + endLine := commentEnd.Line - 1 + endColumn := commentEnd.Column - 1 + if endLine > end.Line || (endLine == end.Line && endColumn > end.Character) { + end.Line = endLine + end.Character = endColumn + } + } + } + + fullRange := &protocol.RangeData{ + Start: start, + End: end, + } + + tag := &protocol.RangeTag{ + Type: rangeType, + Text: obj.Name, + Kind: kind, + FullRange: fullRange, + } + + if deprecated { + tag.Tags = []protocol.SymbolTag{protocol.Deprecated} + } + + return tag +} + // toMarkedString creates a protocol.MarkedString object from the given content. The signature // and extra parameters are formatted as code, if supplied. The docstring is formatted as markdown, // if supplied.