Shorten variable types (#3535)
* Add ShortenType function Taken from https://github.com/aarzilli/gdlv/blob/master/internal/prettyprint/short.go with kind permission by @aarzilli. * Shorten type names in variable values The variables view in VS Code is a lot easier to read if long type names are shortened in much the same way as we shorten them for functions in the call stack view. We only shorten them in the value strings; the Type field of dap.Variable is kept as is. Since this only appears in a tooltip, it isn't a problem to have the full type visible there.
This commit is contained in:
parent
d6f215b27b
commit
aec354cd0b
@ -24,11 +24,13 @@ const (
|
||||
prettyTop prettyFlags = 1 << iota
|
||||
prettyNewlines
|
||||
prettyIncludeType
|
||||
prettyShortenType
|
||||
)
|
||||
|
||||
func (flags prettyFlags) top() bool { return flags&prettyTop != 0 }
|
||||
func (flags prettyFlags) includeType() bool { return flags&prettyIncludeType != 0 }
|
||||
func (flags prettyFlags) newlines() bool { return flags&prettyNewlines != 0 }
|
||||
func (flags prettyFlags) shortenType() bool { return flags&prettyShortenType != 0 }
|
||||
|
||||
func (flags prettyFlags) set(flag prettyFlags, v bool) prettyFlags {
|
||||
if v {
|
||||
@ -45,6 +47,13 @@ func (v *Variable) SinglelineString() string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// SinglelineStringWithShortTypes returns a representation of v on a single line, with types shortened.
|
||||
func (v *Variable) SinglelineStringWithShortTypes() string {
|
||||
var buf bytes.Buffer
|
||||
v.writeTo(&buf, prettyTop|prettyIncludeType|prettyShortenType, "", "")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// SinglelineStringFormatted returns a representation of v on a single line, using the format specified by fmtstr.
|
||||
func (v *Variable) SinglelineStringFormatted(fmtstr string) string {
|
||||
var buf bytes.Buffer
|
||||
@ -59,6 +68,14 @@ func (v *Variable) MultilineString(indent, fmtstr string) string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (v *Variable) typeStr(flags prettyFlags) string {
|
||||
if flags.shortenType() {
|
||||
return ShortenType(v.Type)
|
||||
}
|
||||
|
||||
return v.Type
|
||||
}
|
||||
|
||||
func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
|
||||
if v.Unreadable != "" {
|
||||
fmt.Fprintf(buf, "(unreadable %s)", v.Unreadable)
|
||||
@ -67,7 +84,7 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
|
||||
|
||||
if !flags.top() && v.Addr == 0 && v.Value == "" {
|
||||
if flags.includeType() && v.Type != "void" {
|
||||
fmt.Fprintf(buf, "%s nil", v.Type)
|
||||
fmt.Fprintf(buf, "%s nil", v.typeStr(flags))
|
||||
} else {
|
||||
fmt.Fprint(buf, "nil")
|
||||
}
|
||||
@ -83,10 +100,10 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
|
||||
if v.Type == "" || len(v.Children) == 0 {
|
||||
fmt.Fprint(buf, "nil")
|
||||
} else if v.Children[0].OnlyAddr && v.Children[0].Addr != 0 {
|
||||
v.writePointerTo(buf)
|
||||
v.writePointerTo(buf, flags)
|
||||
} else {
|
||||
if flags.top() && flags.newlines() && v.Children[0].Addr != 0 {
|
||||
v.writePointerTo(buf)
|
||||
v.writePointerTo(buf, flags)
|
||||
fmt.Fprint(buf, "\n")
|
||||
}
|
||||
fmt.Fprint(buf, "*")
|
||||
@ -103,14 +120,14 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
|
||||
v.writeStructTo(buf, flags, indent, fmtstr)
|
||||
} else {
|
||||
if len(v.Children) == 0 {
|
||||
fmt.Fprintf(buf, "%s nil", v.Type)
|
||||
fmt.Fprintf(buf, "%s nil", v.typeStr(flags))
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%s %s/%s", v.Type, v.Children[0].Value, v.Children[1].Value)
|
||||
fmt.Fprintf(buf, "%s %s/%s", v.typeStr(flags), v.Children[0].Value, v.Children[1].Value)
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
if v.Value != "" {
|
||||
fmt.Fprintf(buf, "%s(%s)", v.Type, v.Value)
|
||||
fmt.Fprintf(buf, "%s(%s)", v.typeStr(flags), v.Value)
|
||||
flags = flags.set(prettyIncludeType, false)
|
||||
}
|
||||
v.writeStructTo(buf, flags, indent, fmtstr)
|
||||
@ -123,13 +140,13 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
|
||||
}
|
||||
if flags.includeType() {
|
||||
if v.Children[0].Kind == reflect.Invalid {
|
||||
fmt.Fprintf(buf, "%s ", v.Type)
|
||||
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
|
||||
if v.Children[0].Addr == 0 {
|
||||
fmt.Fprint(buf, "nil")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%s(%s) ", v.Type, v.Children[0].Type)
|
||||
fmt.Fprintf(buf, "%s(%s) ", v.typeStr(flags), v.Children[0].Type)
|
||||
}
|
||||
}
|
||||
data := v.Children[0]
|
||||
@ -145,9 +162,9 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
|
||||
}
|
||||
} else if data.OnlyAddr {
|
||||
if strings.Contains(v.Type, "/") {
|
||||
fmt.Fprintf(buf, "*(*%q)(%#x)", v.Type, v.Addr)
|
||||
fmt.Fprintf(buf, "*(*%q)(%#x)", v.typeStr(flags), v.Addr)
|
||||
} else {
|
||||
fmt.Fprintf(buf, "*(*%s)(%#x)", v.Type, v.Addr)
|
||||
fmt.Fprintf(buf, "*(*%s)(%#x)", v.typeStr(flags), v.Addr)
|
||||
}
|
||||
} else {
|
||||
v.Children[0].writeTo(buf, flags.set(prettyTop, false).set(prettyIncludeType, !flags.includeType()), indent, fmtstr)
|
||||
@ -165,11 +182,11 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Variable) writePointerTo(buf io.Writer) {
|
||||
func (v *Variable) writePointerTo(buf io.Writer, flags prettyFlags) {
|
||||
if strings.Contains(v.Type, "/") {
|
||||
fmt.Fprintf(buf, "(%q)(%#x)", v.Type, v.Children[0].Addr)
|
||||
fmt.Fprintf(buf, "(%q)(%#x)", v.typeStr(flags), v.Children[0].Addr)
|
||||
} else {
|
||||
fmt.Fprintf(buf, "(%s)(%#x)", v.Type, v.Children[0].Addr)
|
||||
fmt.Fprintf(buf, "(%s)(%#x)", v.typeStr(flags), v.Children[0].Addr)
|
||||
}
|
||||
}
|
||||
|
||||
@ -248,7 +265,7 @@ func ExtractIntValue(s string) string {
|
||||
|
||||
func (v *Variable) writeSliceTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
|
||||
if flags.includeType() {
|
||||
fmt.Fprintf(buf, "%s len: %d, cap: %d, ", v.Type, v.Len, v.Cap)
|
||||
fmt.Fprintf(buf, "%s len: %d, cap: %d, ", v.typeStr(flags), v.Len, v.Cap)
|
||||
}
|
||||
if v.Base == 0 && len(v.Children) == 0 {
|
||||
fmt.Fprintf(buf, "nil")
|
||||
@ -259,7 +276,7 @@ func (v *Variable) writeSliceTo(buf io.Writer, flags prettyFlags, indent, fmtstr
|
||||
|
||||
func (v *Variable) writeArrayTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
|
||||
if flags.includeType() {
|
||||
fmt.Fprintf(buf, "%s ", v.Type)
|
||||
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
|
||||
}
|
||||
v.writeSliceOrArrayTo(buf, flags, indent, fmtstr)
|
||||
}
|
||||
@ -267,15 +284,15 @@ func (v *Variable) writeArrayTo(buf io.Writer, flags prettyFlags, indent, fmtstr
|
||||
func (v *Variable) writeStructTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
|
||||
if int(v.Len) != len(v.Children) && len(v.Children) == 0 {
|
||||
if strings.Contains(v.Type, "/") {
|
||||
fmt.Fprintf(buf, "(*%q)(%#x)", v.Type, v.Addr)
|
||||
fmt.Fprintf(buf, "(*%q)(%#x)", v.typeStr(flags), v.Addr)
|
||||
} else {
|
||||
fmt.Fprintf(buf, "(*%s)(%#x)", v.Type, v.Addr)
|
||||
fmt.Fprintf(buf, "(*%s)(%#x)", v.typeStr(flags), v.Addr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if flags.includeType() {
|
||||
fmt.Fprintf(buf, "%s ", v.Type)
|
||||
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
|
||||
}
|
||||
|
||||
nl := v.shouldNewlineStruct(flags.newlines())
|
||||
@ -310,7 +327,7 @@ func (v *Variable) writeStructTo(buf io.Writer, flags prettyFlags, indent, fmtst
|
||||
|
||||
func (v *Variable) writeMapTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
|
||||
if flags.includeType() {
|
||||
fmt.Fprintf(buf, "%s ", v.Type)
|
||||
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
|
||||
}
|
||||
if v.Base == 0 && len(v.Children) == 0 {
|
||||
fmt.Fprintf(buf, "nil")
|
||||
|
102
service/api/shorten_type.go
Normal file
102
service/api/shorten_type.go
Normal file
@ -0,0 +1,102 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func ShortenType(typ string) string {
|
||||
out, ok := shortenTypeEx(typ)
|
||||
if !ok {
|
||||
return typ
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func shortenTypeEx(typ string) (string, bool) {
|
||||
switch {
|
||||
case strings.HasPrefix(typ, "["):
|
||||
for i := range typ {
|
||||
if typ[i] == ']' {
|
||||
sub, ok := shortenTypeEx(typ[i+1:])
|
||||
return typ[:i+1] + sub, ok
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
case strings.HasPrefix(typ, "*"):
|
||||
sub, ok := shortenTypeEx(typ[1:])
|
||||
return "*" + sub, ok
|
||||
case strings.HasPrefix(typ, "map["):
|
||||
depth := 1
|
||||
for i := 4; i < len(typ); i++ {
|
||||
switch typ[i] {
|
||||
case '[':
|
||||
depth++
|
||||
case ']':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
key, keyok := shortenTypeEx(typ[4:i])
|
||||
val, valok := shortenTypeEx(typ[i+1:])
|
||||
return "map[" + key + "]" + val, keyok && valok
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
case typ == "interface {}" || typ == "interface{}":
|
||||
return typ, true
|
||||
case typ == "struct {}" || typ == "struct{}":
|
||||
return typ, true
|
||||
default:
|
||||
if containsAnonymousType(typ) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if lbrk := strings.Index(typ, "["); lbrk >= 0 {
|
||||
if typ[len(typ)-1] != ']' {
|
||||
return "", false
|
||||
}
|
||||
typ0, ok := shortenTypeEx(typ[:lbrk])
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
args := strings.Split(typ[lbrk+1:len(typ)-1], ",")
|
||||
for i := range args {
|
||||
var ok bool
|
||||
args[i], ok = shortenTypeEx(strings.TrimSpace(args[i]))
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return typ0 + "[" + strings.Join(args, ", ") + "]", true
|
||||
}
|
||||
|
||||
slashnum := 0
|
||||
slash := -1
|
||||
for i, ch := range typ {
|
||||
if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' && ch != '.' && ch != '/' && ch != '@' && ch != '%' && ch != '-' {
|
||||
return "", false
|
||||
}
|
||||
if ch == '/' {
|
||||
slash = i
|
||||
slashnum++
|
||||
}
|
||||
}
|
||||
if slashnum <= 1 || slash < 0 {
|
||||
return typ, true
|
||||
}
|
||||
return typ[slash+1:], true
|
||||
}
|
||||
}
|
||||
|
||||
func containsAnonymousType(typ string) bool {
|
||||
for _, thing := range []string{"interface {", "interface{", "struct {", "struct{", "func (", "func("} {
|
||||
idx := strings.Index(typ, thing)
|
||||
if idx >= 0 && idx+len(thing) < len(typ) {
|
||||
ch := typ[idx+len(thing)]
|
||||
if ch != '}' && ch != ')' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
36
service/api/shorten_type_test.go
Normal file
36
service/api/shorten_type_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package api
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestShortenType(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"long/package/path/pkg.A", "pkg.A"},
|
||||
{"[]long/package/path/pkg.A", "[]pkg.A"},
|
||||
{"map[long/package/path/pkg.A]long/package/path/pkg.B", "map[pkg.A]pkg.B"},
|
||||
{"map[long/package/path/pkg.A]interface {}", "map[pkg.A]interface {}"},
|
||||
{"map[long/package/path/pkg.A]interface{}", "map[pkg.A]interface{}"},
|
||||
{"map[long/package/path/pkg.A]struct {}", "map[pkg.A]struct {}"},
|
||||
{"map[long/package/path/pkg.A]struct{}", "map[pkg.A]struct{}"},
|
||||
{"map[long/package/path/pkg.A]map[long/package/path/pkg.B]long/package/path/pkg.C", "map[pkg.A]map[pkg.B]pkg.C"},
|
||||
{"map[long/package/path/pkg.A][]long/package/path/pkg.B", "map[pkg.A][]pkg.B"},
|
||||
{"map[uint64]*github.com/aarzilli/dwarf5/dwarf.typeUnit", "map[uint64]*dwarf.typeUnit"},
|
||||
{"uint8", "uint8"},
|
||||
{"encoding/binary", "encoding/binary"},
|
||||
{"*github.com/go-delve/delve/pkg/proc.Target", "*proc.Target"},
|
||||
{"long/package/path/pkg.Parametric[long/package/path/pkg.A, map[long/package/path/pkg.B]long/package/path/pkg.A]", "pkg.Parametric[pkg.A, map[pkg.B]pkg.A]"},
|
||||
{"[]long/package/path/pkg.Parametric[long/package/path/pkg.A]", "[]pkg.Parametric[pkg.A]"},
|
||||
{"[24]long/package/path/pkg.A", "[24]pkg.A"},
|
||||
{"chan func()", "chan func()"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result := ShortenType(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("shortenTypeEx() got = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -2589,7 +2589,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
|
||||
}
|
||||
return s.variableHandles.create(&fullyQualifiedVariable{v, qualifiedNameOrExpr, false /*not a scope*/, 0})
|
||||
}
|
||||
value = api.ConvertVar(v).SinglelineString()
|
||||
value = api.ConvertVar(v).SinglelineStringWithShortTypes()
|
||||
if v.Unreadable != nil {
|
||||
return value, 0
|
||||
}
|
||||
@ -2603,7 +2603,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
|
||||
// TODO(polina): Get *proc.Variable object from debugger instead. Export a function to set v.loaded to false
|
||||
// and call v.loadValue gain with a different load config. It's more efficient, and it's guaranteed to keep
|
||||
// working with generics.
|
||||
value = api.ConvertVar(v).SinglelineString()
|
||||
value = api.ConvertVar(v).SinglelineStringWithShortTypes()
|
||||
typeName := api.PrettyTypeName(v.DwarfType)
|
||||
loadExpr := fmt.Sprintf("*(*%q)(%#x)", typeName, v.Addr)
|
||||
s.config.log.Debugf("loading %s (type %s) with %s", qualifiedNameOrExpr, typeName, loadExpr)
|
||||
@ -2617,7 +2617,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
|
||||
} else {
|
||||
v.Children = vLoaded.Children
|
||||
v.Value = vLoaded.Value
|
||||
value = api.ConvertVar(v).SinglelineString()
|
||||
value = api.ConvertVar(v).SinglelineStringWithShortTypes()
|
||||
}
|
||||
return value
|
||||
}
|
||||
@ -2649,7 +2649,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
|
||||
} else {
|
||||
cLoaded.Name = v.Children[0].Name // otherwise, this will be the pointer expression
|
||||
v.Children = []proc.Variable{*cLoaded}
|
||||
value = api.ConvertVar(v).SinglelineString()
|
||||
value = api.ConvertVar(v).SinglelineStringWithShortTypes()
|
||||
}
|
||||
} else {
|
||||
value = reloadVariable(v, qualifiedNameOrExpr)
|
||||
|
@ -2502,7 +2502,7 @@ func TestGlobalScopeAndVariables(t *testing.T) {
|
||||
client.VariablesRequest(globalsScope)
|
||||
globals := client.ExpectVariablesResponse(t)
|
||||
checkChildren(t, globals, "Globals", 1)
|
||||
ref := checkVarExact(t, globals, 0, "SomeVar", "github.com/go-delve/delve/_fixtures/internal/dir0/pkg.SomeVar", "github.com/go-delve/delve/_fixtures/internal/dir0/pkg.SomeType {X: 0}", "github.com/go-delve/delve/_fixtures/internal/dir0/pkg.SomeType", hasChildren)
|
||||
ref := checkVarExact(t, globals, 0, "SomeVar", "github.com/go-delve/delve/_fixtures/internal/dir0/pkg.SomeVar", "pkg.SomeType {X: 0}", "github.com/go-delve/delve/_fixtures/internal/dir0/pkg.SomeType", hasChildren)
|
||||
|
||||
if ref > 0 {
|
||||
client.VariablesRequest(ref)
|
||||
|
Loading…
Reference in New Issue
Block a user