Skip to content

Commit

Permalink
Handle code completion and hover
Browse files Browse the repository at this point in the history
  • Loading branch information
harry-hov committed Sep 22, 2023
1 parent e6904ab commit 66473cd
Show file tree
Hide file tree
Showing 4 changed files with 422 additions and 0 deletions.
251 changes: 251 additions & 0 deletions internal/lsp/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package lsp

import (
"context"
"encoding/json"
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"log/slog"
"os"
"path/filepath"
"strings"
"time"

"go.lsp.dev/jsonrpc2"
"go.lsp.dev/protocol"
)

type CompletionStore struct {
time time.Time

pkgs []*Package
}

func (cs *CompletionStore) lookupPkg(pkg string) *Package {
for _, p := range cs.pkgs {
if p.Name == pkg {
return p
}
}
return nil
}

func (cs *CompletionStore) lookupSymbol(pkg, symbol string) *Symbol {
for _, p := range cs.pkgs {
if p.Name == pkg {
for _, s := range p.Symbols {
if s.Name == symbol {
return s
}
}
}
}
return nil
}

func (cs *CompletionStore) lookupSymbolByImports(symbol string, imports []*ast.ImportSpec) *Symbol {
for _, spec := range imports {
value := spec.Path.Value

value = value[1 : len(value)-1] // remove quotes
value = value[strings.LastIndex(value, "/")+1:] // get last part

s := cs.lookupSymbol(value, symbol)
if s != nil {
return s
}
}

return nil
}

type Package struct {
Name string
ImportPath string
Symbols []*Symbol
}

type Symbol struct {
Name string
Doc string
Signature string
Kind string
}

func (s Symbol) String() string {
return fmt.Sprintf("```gno\n%s\n```\n\n%s", s.Signature, s.Doc)
}

func (s *server) Completion(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
var params protocol.CompletionParams
if err := json.Unmarshal(req.Params(), &params); err != nil {
return sendParseError(ctx, reply, err)
}

uri := params.TextDocument.URI
file, ok := s.snapshot.Get(uri.Filename())
if !ok {
return reply(ctx, nil, errors.New("snapshot not found"))
}

items := []protocol.CompletionItem{}

token, err := file.TokenAt(params.Position)
if err != nil {
return reply(ctx, nil, err)
}
text := strings.TrimSuffix(strings.TrimSpace(token.Text), ".")
slog.Info("completion", "text", text)

// TODO:
// pgf, err := file.ParseGno(ctx)
// path, e := astutil.PathEnclosingInterval(pgf.File, 13, 8)

pkg := s.completionStore.lookupPkg(text)
if pkg != nil {
for _, s := range pkg.Symbols {
items = append(items, protocol.CompletionItem{
Label: s.Name,
InsertText: s.Name,
Kind: symbolToKind(s.Kind),
Detail: s.Signature,
Documentation: s.Doc,
})
}
}

return reply(ctx, items, err)
}

func InitCompletionStore(dirs []string) *CompletionStore {
pkgs := []*Package{}

if len(dirs) == 0 {
return &CompletionStore{
pkgs: pkgs,
time: time.Now(),
}
}

pkgDirs, err := ListGnoPackages(dirs)
if err != nil {
panic(err)
}

for _, p := range pkgDirs {
files, err := ListGnoFiles(p)
if err != nil {
panic(err)
}
symbols := []*Symbol{}
for _, file := range files {
symbols = append(symbols, getSymbols(file)...)
}
// convert to import path:
// get path relative to dir, and convert separators to slashes.
ip := strings.ReplaceAll(
strings.TrimPrefix(p, p+string(filepath.Separator)),
string(filepath.Separator), "/",
)

pkgs = append(pkgs, &Package{
Name: filepath.Base(p),
ImportPath: ip,
Symbols: symbols,
})
}

return &CompletionStore{
pkgs: pkgs,
time: time.Now(),
}
}

func getSymbols(fname string) []*Symbol {
var symbols []*Symbol

// Create a FileSet to work with.
fset := token.NewFileSet()

// Parse the file and create an AST.
file, err := parser.ParseFile(fset, fname, nil, parser.ParseComments)
if err != nil {
panic(err)
}

bsrc, err := os.ReadFile(fname)
if err != nil {
panic(err)
}
text := string(bsrc)

// Trim AST to exported declarations only.
ast.FileExports(file)

ast.Inspect(file, func(n ast.Node) bool {
var found *Symbol

switch n.(type) {
case *ast.FuncDecl:
found = function(n, text)
case *ast.GenDecl:
found = declaration(n, text)
}

if found != nil {
symbols = append(symbols, found)
}

return true
})

return symbols
}

func declaration(n ast.Node, source string) *Symbol {
sym, _ := n.(*ast.GenDecl)

for _, spec := range sym.Specs {
switch t := spec.(type) {
case *ast.TypeSpec:
return &Symbol{
Name: t.Name.Name,
Doc: sym.Doc.Text(),
Signature: strings.Split(source[t.Pos()-1:t.End()-1], " {")[0],
Kind: typeName(*t),
}
}
}

return nil
}

func function(n ast.Node, source string) *Symbol {
sym, _ := n.(*ast.FuncDecl)
return &Symbol{
Name: sym.Name.Name,
Doc: sym.Doc.Text(),
Signature: strings.Split(source[sym.Pos()-1:sym.End()-1], " {")[0],
Kind: "func",
}
}

func typeName(t ast.TypeSpec) string {
switch t.Type.(type) {
case *ast.StructType:
return "struct"
case *ast.InterfaceType:
return "interface"
case *ast.ArrayType:
return "array"
case *ast.MapType:
return "map"
case *ast.ChanType:
return "chan"
default:
return "type"
}
}
97 changes: 97 additions & 0 deletions internal/lsp/hover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package lsp

import (
"context"
"encoding/json"
"errors"
"log/slog"
"strings"

"go.lsp.dev/jsonrpc2"
"go.lsp.dev/protocol"
)

type HoveredToken struct {
Text string
Start int
End int
}

func (s *server) Hover(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
var params protocol.DefinitionParams
if err := json.Unmarshal(req.Params(), &params); err != nil {
return sendParseError(ctx, reply, err)
}

uri := params.TextDocument.URI
file, ok := s.snapshot.Get(uri.Filename())
if !ok {
return reply(ctx, nil, errors.New("snapshot not found"))
}

offset := file.PositionToOffset(params.Position)
// tokedf := pgf.FileSet.AddFile(doc.Path, -1, len(doc.Content))
// target := tokedf.Pos(offset)

slog.Info("hover", "offset", offset)
pgf, err := file.ParseGno(ctx)
if err != nil {
reply(ctx, nil, errors.New("cannot parse gno file"))
}
for _, spec := range pgf.File.Imports {
slog.Info("hover", "spec", spec.Path.Value, "pos", spec.Path.Pos(), "end", spec.Path.End())
if int(spec.Path.Pos()) <= offset && offset <= int(spec.Path.End()) {
// TODO: handle hover for imports
slog.Info("hover", "import", spec.Path.Value)
return reply(ctx, nil, nil)
}
}

token, err := file.TokenAt(params.Position)
if err != nil {
return reply(ctx, protocol.Hover{}, err)
}
text := strings.TrimSpace(token.Text)

// FIXME: Use the AST package to do this + get type of token.
//
// This is just a quick PoC to get something working.

// strings.Split(p.Body,
text = strings.Split(text, "(")[0]

text = strings.TrimSuffix(text, ",")
text = strings.TrimSuffix(text, ")")

// *mux.Request
text = strings.TrimPrefix(text, "*")

slog.Info("hover", "pkg", len(s.completionStore.pkgs))

parts := strings.Split(text, ".")
if len(parts) == 2 {
pkg := parts[0]
sym := parts[1]

slog.Info("hover", "pkg", pkg, "sym", sym)
found := s.completionStore.lookupSymbol(pkg, sym)
if found == nil && pgf.File != nil {
found = s.completionStore.lookupSymbolByImports(sym, pgf.File.Imports)
}

if found != nil {
return reply(ctx, protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.Markdown,
Value: found.String(),
},
Range: posToRange(
int(params.Position.Line),
[]int{token.Start, token.End},
),
}, nil)
}
}

return reply(ctx, nil, err)
}
6 changes: 6 additions & 0 deletions internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type server struct {
env *env.Env

snapshot *Snapshot
completionStore *CompletionStore

formatOpt tools.FormattingOption
}
Expand All @@ -36,6 +37,7 @@ func BuildServerHandler(conn jsonrpc2.Conn, env *env.Env) jsonrpc2.Handler {
env: env,

snapshot: NewSnapshot(),
completionStore: InitCompletionStore(dirs),

formatOpt: tools.Gofumpt,
}
Expand Down Expand Up @@ -63,6 +65,10 @@ func (s *server) ServerHandler(ctx context.Context, reply jsonrpc2.Replier, req
return s.DidSave(ctx, reply, req)
case "textDocument/formatting":
return s.Formatting(ctx, reply, req)
case "textDocument/hover":
return s.Hover(ctx, reply, req)
case "textDocument/completion":
return s.Completion(ctx, reply, req)
default:
return jsonrpc2.MethodNotFoundHandler(ctx, reply, req)
}
Expand Down
Loading

0 comments on commit 66473cd

Please sign in to comment.