Skip to content
This repository was archived by the owner on Sep 26, 2023. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions internal/indexer/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand Down
71 changes: 71 additions & 0 deletions internal/indexer/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand All @@ -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.
Expand Down