From e9cad3e85971163fdd2a155c7030d9f70e1c1e89 Mon Sep 17 00:00:00 2001 From: Hariom Verma Date: Fri, 29 Mar 2024 01:55:37 +0530 Subject: [PATCH] Hover support for builtins - Adds hover support for builtin types and functions. - Contains fixex --- go.mod | 3 +- internal/lsp/builtin.go | 129 ++++++++++++++++++ internal/lsp/check.go | 37 +++++- internal/lsp/completion.go | 2 + internal/lsp/hover.go | 265 +++++++++++++++++++++++++++++++------ 5 files changed, 390 insertions(+), 46 deletions(-) create mode 100644 internal/lsp/builtin.go diff --git a/go.mod b/go.mod index 140abe6..1ff6ca1 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,9 @@ require ( go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 go.lsp.dev/protocol v0.12.0 go.lsp.dev/uri v0.3.0 + go.uber.org/multierr v1.9.0 golang.org/x/mod v0.14.0 + golang.org/x/text v0.14.0 mvdan.cc/gofumpt v0.4.0 ) @@ -52,7 +54,6 @@ require ( go.etcd.io/bbolt v1.3.8 // indirect go.opencensus.io v0.22.5 // indirect go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.15.0 // indirect golang.org/x/net v0.17.0 // indirect diff --git a/internal/lsp/builtin.go b/internal/lsp/builtin.go new file mode 100644 index 0000000..d5ee12f --- /dev/null +++ b/internal/lsp/builtin.go @@ -0,0 +1,129 @@ +package lsp + +import ( + "go/ast" + "go/types" + "strings" +) + +// Builtin types +const ( + boolDoc = "bool is the set of boolean values, true and false." + byteDoc = "byte is an alias for uint8 and is equivalent to uint8 in all ways. It is used, by convention, to distinguish byte values from 8-bit unsigned integer values." + errorDoc = "The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error." + intDoc = "int is a signed integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, int32." + int8Doc = "int8 is the set of all signed 8-bit integers. Range: -128 through 127." + int16Doc = "int16 is the set of all signed 16-bit integers. Range: -32768 through 32767." + int32Doc = "int32 is the set of all signed 32-bit integers. Range: -2147483648 through 2147483647." + int64Doc = "int64 is the set of all signed 64-bit integers. Range: -9223372036854775808 through 9223372036854775807." + uintDoc = "uint is an unsigned integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, uint32." + uint8Doc = "uint8 is the set of all unsigned 8-bit integers. Range: 0 through 255." + uint16Doc = "uint16 is the set of all unsigned 16-bit integers. Range: 0 through 65535." + uint32Doc = "uint32 is the set of all unsigned 32-bit integers. Range: 0 through 4294967295." + uint64Doc = "uint64 is the set of all unsigned 64-bit integers. Range: 0 through 18446744073709551615." + float32Doc = "float32 is the set of all IEEE-754 32-bit floating-point numbers." + float64Doc = "float64 is the set of all IEEE-754 64-bit floating-point numbers." + runeDoc = "rune is an alias for int32 and is equivalent to int32 in all ways. It is used, by convention, to distinguish character values from integer values." + stringDoc = "string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. A string may be empty, but not nil. Values of string type are immutable." + nilDoc = "nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type." +) + +// Builtin funcs +const ( + appendDoc = "The append built-in function appends elements to the end of a slice. If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated. Append returns the updated slice." + capDoc = "The cap built-in function returns the capacity of v, according to its type" + clearDoc = "The clear built-in function clears maps and slices. For maps, clear deletes all entries, resulting in an empty map. For slices, clear sets all elements up to the length of the slice to the zero value of the respective element type." + copyDoc = "The copy built-in function copies elements from a source slice into a destination slice. (As a special case, it also will copy bytes from a string to a slice of bytes.) The source and destination may overlap." + deleteDoc = "The delete built-in function deletes the element with the specified key (m[key]) from the map. If m is nil or there is no such element, delete is a no-op." + lenDoc = "The len built-in function returns the length of v, according to its type" + makeDoc = "The make built-in function allocates and initializes an object of type slice, map, or chan (only). Like new, the first argument is a type, not a value. Unlike new, make's return type is the same as the type of its argument, not a pointer to it." + newDoc = "The new built-in function allocates memory. The first argument is a type, not a value, and the value returned is a pointer to a newly allocated zero value of that type." + panicDoc = "The panic built-in function stops normal execution of the current goroutine. When a function F calls panic, normal execution of F stops immediately." + printDoc = "The print built-in function formats its arguments in an implementation-specific way and writes the result to standard error. Print is useful for bootstrapping and debugging." + printlnDoc = "The println built-in function formats its arguments in an implementation-specific way and writes the result to standard error. Spaces are always added between arguments and a newline is appended." + recoverDoc = "The recover built-in function allows a program to manage behavior of a panicking goroutine. Executing a call to recover inside a deferred function (but not any function called by it) stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function it will not stop a panicking sequence." +) + +func isBuiltin(i *ast.Ident, tv *types.TypeAndValue) (string, bool) { + t := tv.Type.String() + name := i.Name + if strings.Contains(t, "gno.land/") { + return "", false + } + + if name == "nil" && t == "untyped nil" { // special case? + return nilDoc, true + } + if (name == "true" || name == "false") && t == "bool" { // special case? + return boolDoc, true + } + if name == t { // hover on the type itself? + switch t { + case "byte": + return byteDoc, true + case "error": + return errorDoc, true + case "int": + return intDoc, true + case "int8": + return int8Doc, true + case "int16": + return int16Doc, true + case "int32": + return int32Doc, true + case "int64": + return int64Doc, true + case "uint": + return uintDoc, true + case "uint8": + return uint8Doc, true + case "uint16": + return uint16Doc, true + case "uint32": + return uint32Doc, true + case "uint64": + return uint64Doc, true + case "float32": + return float32Doc, true + case "float64": + return float64Doc, true + case "rune": + return runeDoc, true + case "string": + return stringDoc, true + case "nil": + return nilDoc, true + } + } + + if strings.HasPrefix(t, "func") { + switch name { + case "append": + return appendDoc, true + case "cap": + return capDoc, true + case "clear": + return clearDoc, true + case "copy": + return copyDoc, true + case "delete": + return deleteDoc, true + case "len": + return lenDoc, true + case "make": + return makeDoc, true + case "new": + return newDoc, true + case "panic": + return panicDoc, true + case "print": + return printDoc, true + case "println": + return printlnDoc, true + case "recover": + return recoverDoc, true + } + } + + return "", false +} diff --git a/internal/lsp/check.go b/internal/lsp/check.go index 28ea7c8..7b607a3 100644 --- a/internal/lsp/check.go +++ b/internal/lsp/check.go @@ -185,7 +185,7 @@ func (tcr *TypeCheckResult) Errors() []ErrorInfo { Column: col, Span: []int{col, math.MaxInt}, Msg: msg, - Tool: "go/typecheck", + Tool: "typecheck", }) } return res @@ -212,12 +212,43 @@ func formatTypeInfo(fset token.FileSet, info *types.Info) string { return strings.Join(items, "\n") } +// Prints types.Info in a tabular form +// Kept only for debugging purpose. +func getTypeAndValue( + fset token.FileSet, + info *types.Info, + tok string, + line, offset int, +) (ast.Expr, *types.TypeAndValue) { + for expr, tv := range info.Types { + if tok != types.ExprString(expr) { + continue + } + posn := fset.Position(expr.Pos()) + if line != posn.Line { + continue + } + slog.Info("getTypeInfo", "offset", offset, "pos", expr.Pos(), "end", expr.End()) + if token.Pos(offset) < expr.Pos() && + token.Pos(offset) > expr.End() { + continue + } + + // tv.Value. + tvstr := tv.Type.String() + if tv.Value != nil { + tvstr += " = " + tv.Value.String() + } + + return expr, &tv + } + return nil, nil +} + func mode(tv types.TypeAndValue) string { switch { case tv.IsVoid(): return "void" - case tv.IsType(): - return "type" case tv.IsBuiltin(): return "builtin" case tv.IsNil(): diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index 0f50c84..056b6e6 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -75,6 +75,8 @@ type Package struct { Functions []*Function Methods cmap.ConcurrentMap[string, []*Method] Structures []*Structure + + TypeCheckResult *TypeCheckResult } type Symbol struct { diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index 0d9e548..c600619 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "go/ast" + "go/token" + "go/types" "log/slog" "path/filepath" "strings" @@ -27,38 +29,92 @@ func (s *server) Hover(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2 } uri := params.TextDocument.URI + + // Get snapshot of the current file file, ok := s.snapshot.Get(uri.Filename()) if !ok { return reply(ctx, nil, errors.New("snapshot not found")) } + // Try parsing current file + pgf, err := file.ParseGno(ctx) + if err != nil { + return reply(ctx, nil, errors.New("cannot parse gno file")) + } + // Load pkg from cache + pkg, ok := s.cache.pkgs.Get(filepath.Dir(string(params.TextDocument.URI.Filename()))) + if !ok { + return reply(ctx, nil, nil) + } + // Calculate offset and line offset := file.PositionToOffset(params.Position) - line := params.Position.Line - // tokedf := pgf.FileSet.AddFile(doc.Path, -1, len(doc.Content)) - // target := tokedf.Pos(offset) + line := params.Position.Line + 1 // starts at 0, so adding 1 slog.Info("hover", "line", line, "offset", offset) - pgf, err := file.ParseGno(ctx) - if err != nil { - return reply(ctx, nil, errors.New("cannot parse gno file")) + + // Handle hovering over import paths + for _, spec := range pgf.File.Imports { + // Inclusive of the end points + if spec.Path.Pos() <= token.Pos(offset) && token.Pos(offset) <= spec.Path.End() { + return hoverImport(ctx, reply, pgf, params, spec) + } } - var expr ast.Expr - ast.Inspect(pgf.File, func(n ast.Node) bool { - if e, ok := n.(ast.Expr); ok && pgf.Fset.Position(e.Pos()).Line == int(line+1) { - expr = e - return false + // Get path enclosing + path := pathEnclosingObjNode(pgf.File, token.Pos(offset)) + if len(path) < 1 { + return reply(ctx, nil, nil) + } + info := pkg.TypeCheckResult.info + + switch i := path[0].(type) { + case *ast.Ident: + _, tv := getTypeAndValue( + *pkg.TypeCheckResult.fset, + info, i.Name, + int(line), + offset, + ) + if tv == nil || tv.Type == nil { + break } - return true - }) + typeStr := tv.Type.String() + + // local - // TODO: Remove duplicate code - switch e := expr.(type) { + // Handle builtins + if doc, ok := isBuiltin(i, tv); ok { + return hoverBuiltinTypes(ctx, reply, params, i, tv, doc) + } + + header := fmt.Sprintf("%s %s %s", mode(*tv), i.Name, typeStr) + return reply(ctx, protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: protocol.Markdown, + Value: FormatHoverContent(header, ""), + }, + Range: posToRange( + int(params.Position.Line), + []int{int(i.Pos()), int(i.End())}, + ), + }, nil) + default: + return reply(ctx, nil, nil) + } + + expr := getExprAtLine(pgf, int(line)) + if expr == nil { + return reply(ctx, nil, nil) + } + switch e := expr.(type) { // TODO: Remove duplicate code case *ast.CallExpr: slog.Info("hover - CALL_EXPR") switch v := e.Fun.(type) { case *ast.Ident: // TODO: don't show methods + if offset < int(v.Pos()) && offset > int(v.End()) { + break + } pkgPath := filepath.Dir(params.TextDocument.URI.Filename()) sym, ok := s.cache.lookupSymbol(pkgPath, v.Name) if !ok { @@ -83,9 +139,9 @@ func (s *server) Hover(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2 if offset >= int(i.Pos())-1 && offset < int(i.End())-1 { // pkg or var if i.Obj != nil { // var - return s.hoverVariableIdent(ctx, reply, pgf, params, i) + return hoverVariableIdent(ctx, reply, pgf, params, i) } - return s.hoverPackageIdent(ctx, reply, pgf, params, i) + return hoverPackageIdent(ctx, reply, pgf, params, i) } else if offset >= int(e.Pos())-1 && offset < int(e.End())-1 { // Func symbol := s.completionStore.lookupSymbol(i.Name, v.Sel.Name) if symbol != nil { @@ -113,10 +169,10 @@ func (s *server) Hover(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2 return reply(ctx, nil, nil) } if i.Obj != nil { // its a var - return s.hoverVariableIdent(ctx, reply, pgf, params, i) + return hoverVariableIdent(ctx, reply, pgf, params, i) } if offset >= int(i.Pos())-1 && offset < int(i.End())-1 { // X - return s.hoverPackageIdent(ctx, reply, pgf, params, i) + return hoverPackageIdent(ctx, reply, pgf, params, i) } else if offset >= int(e.Pos())-1 && offset < int(e.End())-1 { // A symbol := s.completionStore.lookupSymbol(i.Name, e.Sel.Name) if symbol != nil { @@ -151,10 +207,6 @@ func (s *server) Hover(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2 switch t := funcDecl.Recv.List[0].Type.(type) { case *ast.StarExpr: k := fmt.Sprintf("*%s", t.X) - pkg, ok := s.cache.pkgs.Get(filepath.Dir(string(params.TextDocument.URI.Filename()))) - if !ok { - return reply(ctx, nil, nil) - } var structure *Structure for _, st := range pkg.Structures { if st.Name == fmt.Sprintf("%s", t.X) { @@ -210,7 +262,62 @@ func (s *server) Hover(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2 return reply(ctx, nil, nil) } -func (s *server) hoverPackageIdent(ctx context.Context, reply jsonrpc2.Replier, pgf *ParsedGnoFile, params protocol.HoverParams, i *ast.Ident) error { +func hoverBuiltinTypes(ctx context.Context, reply jsonrpc2.Replier, params protocol.HoverParams, i *ast.Ident, tv *types.TypeAndValue, doc string) error { + t := tv.Type.String() + m := mode(*tv) + var header string + if t == "nil" || t == "untyped nil" { // special case? + header = "var nil Type" + } else if strings.HasPrefix(t, "func") && m == "builtin" { + header = i.Name + strings.TrimPrefix(t, "func") + } else if (i.Name == "true" || i.Name == "false") && t == "bool" { + header = `const ( + true = 0 == 0 // Untyped bool. + false = 0 != 0 // Untyped bool. +)` + } else { + header = fmt.Sprintf("%s %s %s", m, i.Name, t) + } + + return reply(ctx, protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: protocol.Markdown, + Value: FormatHoverContent(header, doc), + }, + Range: posToRange( + int(params.Position.Line), + []int{int(i.Pos()), int(i.End())}, + ), + }, nil) +} + +// TODO: check if imports exists in `examples` or `stdlibs` +func hoverImport(ctx context.Context, reply jsonrpc2.Replier, pgf *ParsedGnoFile, params protocol.HoverParams, spec *ast.ImportSpec) error { + // remove leading and trailing `"` + path := spec.Path.Value[1 : len(spec.Path.Value)-1] + parts := strings.Split(path, "/") + last := parts[len(parts)-1] + + header := fmt.Sprintf("package %s (%s)", last, spec.Path.Value) + body := func() string { + if strings.HasPrefix(path, "gno.land/") { + return fmt.Sprintf("[```%s``` on gno.land](https://%s)", last, path) + } + return fmt.Sprintf("[```%s``` on gno.land](https://gno.land)", last) + }() + return reply(ctx, protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: protocol.Markdown, + Value: FormatHoverContent(header, body), + }, + Range: posToRange( + int(params.Position.Line), + []int{int(spec.Pos()), int(spec.End())}, + ), + }, nil) +} + +func hoverPackageIdent(ctx context.Context, reply jsonrpc2.Replier, pgf *ParsedGnoFile, params protocol.HoverParams, i *ast.Ident) error { for _, spec := range pgf.File.Imports { // remove leading and trailing `"` path := spec.Path.Value[1 : len(spec.Path.Value)-1] @@ -239,24 +346,7 @@ func (s *server) hoverPackageIdent(ctx context.Context, reply jsonrpc2.Replier, return reply(ctx, nil, nil) } -// getIdentNodes return idents from Expr -// Note: only handles *ast.SelectorExpr and *ast.CallExpr -func getIdentNodes(n ast.Node) []*ast.Ident { - res := []*ast.Ident{} - switch t := n.(type) { - case *ast.Ident: - res = append(res, t) - case *ast.SelectorExpr: - res = append(res, t.Sel) - res = append(res, getIdentNodes(t.X)...) - case *ast.CallExpr: - res = append(res, getIdentNodes(t.Fun)...) - } - - return res -} - -func (s *server) hoverVariableIdent(ctx context.Context, reply jsonrpc2.Replier, pgf *ParsedGnoFile, params protocol.HoverParams, i *ast.Ident) error { +func hoverVariableIdent(ctx context.Context, reply jsonrpc2.Replier, pgf *ParsedGnoFile, params protocol.HoverParams, i *ast.Ident) error { if i.Obj != nil { switch u := i.Obj.Decl.(type) { case *ast.Field: @@ -356,3 +446,94 @@ func (s *server) hoverVariableIdent(ctx context.Context, reply jsonrpc2.Replier, func FormatHoverContent(header, body string) string { return fmt.Sprintf("```gno\n%s\n```\n\n%s", header, body) } + +// getIdentNodes return idents from Expr +// Note: only handles *ast.SelectorExpr and *ast.CallExpr +func getIdentNodes(n ast.Node) []*ast.Ident { + res := []*ast.Ident{} + switch t := n.(type) { + case *ast.Ident: + res = append(res, t) + case *ast.SelectorExpr: + res = append(res, t.Sel) + res = append(res, getIdentNodes(t.X)...) + case *ast.CallExpr: + res = append(res, getIdentNodes(t.Fun)...) + } + + return res +} + +func getExprAtLine(pgf *ParsedGnoFile, line int) ast.Expr { + var expr ast.Expr + ast.Inspect(pgf.File, func(n ast.Node) bool { + if e, ok := n.(ast.Expr); ok && pgf.Fset.Position(e.Pos()).Line == int(line) { + expr = e + return false + } + return true + }) + return expr +} + +// pathEnclosingObjNode returns the AST path to the object-defining +// node associated with pos. "Object-defining" means either an +// *ast.Ident mapped directly to a types.Object or an ast.Node mapped +// implicitly to a types.Object. +func pathEnclosingObjNode(f *ast.File, pos token.Pos) []ast.Node { + var ( + path []ast.Node + found bool + ) + + ast.Inspect(f, func(n ast.Node) bool { + if found { + return false + } + + if n == nil { + path = path[:len(path)-1] + return false + } + + path = append(path, n) + + switch n := n.(type) { + case *ast.Ident: + // Include the position directly after identifier. This handles + // the common case where the cursor is right after the + // identifier the user is currently typing. Previously we + // handled this by calling astutil.PathEnclosingInterval twice, + // once for "pos" and once for "pos-1". + found = n.Pos() <= pos && pos <= n.End() + case *ast.ImportSpec: + if n.Path.Pos() <= pos && pos < n.Path.End() { + found = true + // If import spec has a name, add name to path even though + // position isn't in the name. + if n.Name != nil { + path = append(path, n.Name) + } + } + case *ast.StarExpr: + // Follow star expressions to the inner identifier. + if pos == n.Star { + pos = n.X.Pos() + } + } + + return !found + }) + + if len(path) == 0 { + return nil + } + + // Reverse path so leaf is first element. + for i := 0; i < len(path)/2; i++ { + path[i], path[len(path)-1-i] = path[len(path)-1-i], path[i] + } + + return path +} +