dap: add 'clipboard' support, and truncate a long value (#2513)

- add 'clipboard' capability
- apply a larger string limit for 'hover' and 'clipboard' context
- truncate the string representation of compound (or pointer of compound)
type variable
This commit is contained in:
Hyang-Ah Hana Kim
2021-06-04 03:27:57 -04:00
committed by GitHub
parent 054e3f8ef2
commit 152c74e94e
3 changed files with 76 additions and 28 deletions

View File

@ -104,6 +104,7 @@ func (c *Client) ExpectInitializeResponseAndCapabilities(t *testing.T) *dap.Init
SupportsSetVariable: true,
SupportsFunctionBreakpoints: true,
SupportsEvaluateForHovers: true,
SupportsClipboardContext: true,
}
if !reflect.DeepEqual(initResp.Body, wantCapabilities) {
t.Errorf("capabilities in initializeResponse: got %+v, want %v", pretty(initResp.Body), pretty(wantCapabilities))

View File

@ -688,6 +688,7 @@ func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
response.Body.SupportsExceptionInfoRequest = true
response.Body.SupportsSetVariable = true
response.Body.SupportsEvaluateForHovers = true
response.Body.SupportsClipboardContext = true
// TODO(polina): support these requests in addition to vscode-go feature parity
response.Body.SupportsTerminateRequest = false
response.Body.SupportsRestartRequest = false
@ -1814,26 +1815,42 @@ func (s *Server) getTypeIfSupported(v *proc.Variable) string {
// custom, a zero reference, reminiscent of a zero pointer, is used to indicate that
// a scalar variable cannot be "dereferenced" to get its elements (as there are none).
func (s *Server) convertVariable(v *proc.Variable, qualifiedNameOrExpr string) (value string, variablesReference int) {
return s.convertVariableWithOpts(v, qualifiedNameOrExpr, false)
return s.convertVariableWithOpts(v, qualifiedNameOrExpr, 0)
}
func (s *Server) convertVariableToString(v *proc.Variable) string {
val, _ := s.convertVariableWithOpts(v, "", true)
val, _ := s.convertVariableWithOpts(v, "", skipRef)
return val
}
// defaultMaxValueLen is the max length of a string representation of a compound or reference
// type variable.
const defaultMaxValueLen = 1 << 8 // 256
// Flags for convertVariableWithOpts option.
type convertVariableFlags uint8
const (
skipRef convertVariableFlags = 1 << iota
showFullValue
)
// convertVariableWithOpts allows to skip reference generation in case all we need is
// a string representation of the variable.
func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr string, skipRef bool) (value string, variablesReference int) {
// a string representation of the variable. When the variable is a compound or reference
// type variable and its full string representation can be larger than defaultMaxValueLen,
// this returns a truncated value unless showFull option flag is set.
func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr string, opts convertVariableFlags) (value string, variablesReference int) {
canHaveRef := false
maybeCreateVariableHandle := func(v *proc.Variable) int {
if skipRef {
canHaveRef = true
if opts&skipRef != 0 {
return 0
}
return s.variableHandles.create(&fullyQualifiedVariable{v, qualifiedNameOrExpr, false /*not a scope*/})
}
value = api.ConvertVar(v).SinglelineString()
if v.Unreadable != nil {
return value, variablesReference
return value, 0
}
// Some of the types might be fully or partially not loaded based on LoadConfig.
@ -1962,6 +1979,10 @@ func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr s
variablesReference = maybeCreateVariableHandle(v)
}
}
canTruncateValue := showFullValue&opts == 0
if len(value) > defaultMaxValueLen && canTruncateValue && canHaveRef {
value = value[:defaultMaxValueLen] + "..."
}
return value, variablesReference
}
@ -2019,21 +2040,27 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
return
}
if exprVar.Kind == reflect.String && (request.Arguments.Context == "repl" || request.Arguments.Context == "variables") {
if strVal := constant.StringVal(exprVar.Value); exprVar.Len > int64(len(strVal)) {
// reload string value with a bigger limit.
loadCfg := DefaultLoadConfig
loadCfg.MaxStringLen = 4 << 10
if v, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, loadCfg); err != nil {
s.log.Debugf("Failed to load more for %v: %v", request.Arguments.Expression, err)
} else {
exprVar = v
ctxt := request.Arguments.Context
switch ctxt {
case "repl", "variables", "hover", "clipboard":
if exprVar.Kind == reflect.String {
if strVal := constant.StringVal(exprVar.Value); exprVar.Len > int64(len(strVal)) {
// reload string value with a bigger limit.
loadCfg := DefaultLoadConfig
loadCfg.MaxStringLen = 4 << 10
if v, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, loadCfg); err != nil {
s.log.Debugf("Failed to load more for %v: %v", request.Arguments.Expression, err)
} else {
exprVar = v
}
}
}
}
// TODO(polina): as far as I can tell, evaluateName is ignored by vscode for expression variables.
// Should it be skipped altogether for all levels?
exprVal, exprRef := s.convertVariable(exprVar, fmt.Sprintf("(%s)", request.Arguments.Expression))
var opts convertVariableFlags
if ctxt == "variables" || ctxt == "hover" || ctxt == "clipboard" {
opts |= showFullValue
}
exprVal, exprRef := s.convertVariableWithOpts(exprVar, fmt.Sprintf("(%s)", request.Arguments.Expression), opts)
response.Body = dap.EvaluateResponseBody{Result: exprVal, VariablesReference: exprRef}
}
s.send(response)

View File

@ -1493,7 +1493,7 @@ func TestVariablesLoading(t *testing.T) {
}
// Map partially missing based on LoadConfig.MaxArrayValues
ref = checkVarRegex(t, locals, -1, "m1", "m1", `\(loaded 64/66\) map\[string\]main\.astruct \[.+\.\.\.\+2 more\]`, `map\[string\]main\.astruct`, hasChildren)
ref = checkVarRegex(t, locals, -1, "m1", "m1", `\(loaded 64/66\) map\[string\]main\.astruct \[.+\.\.\.`, `map\[string\]main\.astruct`, hasChildren)
if ref > 0 {
client.VariablesRequest(ref)
m1 := client.ExpectVariablesResponse(t)
@ -1572,7 +1572,7 @@ func TestVariablesLoading(t *testing.T) {
client.VariablesRequest(ref)
tmV := client.ExpectVariablesResponse(t)
checkChildren(t, tmV, "tm.v", 1)
ref = checkVarRegex(t, tmV, 0, `\[0\]`, `tm\.v\[0\]`, `map\[string\]main\.astruct \[.+\.\.\.\+2 more\]`, `map\[string\]main\.astruct`, hasChildren)
ref = checkVarRegex(t, tmV, 0, `\[0\]`, `tm\.v\[0\]`, `map\[string\]main\.astruct \[.+\.\.\.`, `map\[string\]main\.astruct`, hasChildren)
if ref > 0 {
client.VariablesRequest(ref)
tmV0 := client.ExpectVariablesResponse(t)
@ -1602,7 +1602,7 @@ func TestVariablesLoading(t *testing.T) {
tmV := client.ExpectVariablesResponse(t)
checkChildren(t, tmV, "tm.v", 1)
// TODO(polina): this evaluate name is not usable - it should be empty
ref = checkVarRegex(t, tmV, 0, `\[0\]`, `\[0\]`, `map\[string\]main\.astruct \[.+\.\.\.\+2 more\]`, `map\[string\]main\.astruct`, hasChildren)
ref = checkVarRegex(t, tmV, 0, `\[0\]`, `\[0\]`, `map\[string\]main\.astruct \[.+\.\.\.`, `map\[string\]main\.astruct`, hasChildren)
if ref > 0 {
client.VariablesRequest(ref)
tmV0 := client.ExpectVariablesResponse(t)
@ -2723,13 +2723,18 @@ func TestEvaluateRequest(t *testing.T) {
if erres.Body.Error.Format != "Unable to evaluate expression: could not find symbol value for a1" {
t.Errorf("\ngot %#v\nwant Format=\"Unable to evaluate expression: could not find symbol value for a1\"", erres)
}
client.EvaluateRequest("a1", 1002, "clipboard")
erres = client.ExpectVisibleErrorResponse(t)
if erres.Body.Error.Format != "Unable to evaluate expression: could not find symbol value for a1" {
t.Errorf("\ngot %#v\nwant Format=\"Unable to evaluate expression: could not find symbol value for a1\"", erres)
}
},
disconnect: false,
}})
})
}
func TestEvaluateRequestLongStr(t *testing.T) {
func TestEvaluateRequestLongStrLargeValue(t *testing.T) {
runTest(t, "testvariables2", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client, "launch",
// Launch
@ -2741,22 +2746,24 @@ func TestEvaluateRequestLongStr(t *testing.T) {
[]onBreakpoint{{
execute: func() {
longstr := `"very long string 0123456789a0123456789b0123456789c0123456789d0123456789e0123456789f0123456789g012345678h90123456789i0123456789j0123456789"`
longstrTruncated := `"very long string 0123456789a0123456789b0123456789c0123456789d012...+73 more"`
checkStop(t, client, 1, "main.main", -1)
client.VariablesRequest(1001) // Locals
locals := client.ExpectVariablesResponse(t)
// reflect.Kind == String, load with longer load limit if evaluated in repl/variables context.
for _, evalContext := range []string{"", "watch", "repl", "variables", "somethingelse"} {
for _, evalContext := range []string{"", "watch", "repl", "variables", "hover", "clipboard", "somethingelse"} {
t.Run(evalContext, func(t *testing.T) {
// long string
longstr := `"very long string 0123456789a0123456789b0123456789c0123456789d0123456789e0123456789f0123456789g012345678h90123456789i0123456789j0123456789"`
longstrTruncated := `"very long string 0123456789a0123456789b0123456789c0123456789d012...+73 more"`
client.EvaluateRequest("longstr", 0, evalContext)
got := client.ExpectEvaluateResponse(t)
want := longstrTruncated
if evalContext == "repl" || evalContext == "variables" {
switch evalContext {
case "repl", "variables", "hover", "clipboard":
want = longstr
}
checkEval(t, got, want, false)
@ -2769,9 +2776,22 @@ func TestEvaluateRequestLongStr(t *testing.T) {
// variables are not affected.
checkVarExact(t, locals, -1, "longstr", "longstr", longstrTruncated, "string", noChildren)
checkVarExact(t, locals, -1, "m6", "m6", `main.C {s: `+longstrTruncated+`}`, "main.C", hasChildren)
// large array
m1 := `\(loaded 64/66\) map\[string\]main\.astruct \[.+\.\.\.\+2 more\]`
m1Truncated := `\(loaded 64/66\) map\[string\]main\.astruct \[.+\.\.\.`
client.EvaluateRequest("m1", 0, evalContext)
got3 := client.ExpectEvaluateResponse(t)
want3 := m1Truncated
switch evalContext {
case "variables", "hover", "clipboard":
want3 = m1
}
checkEvalRegex(t, got3, want3, true)
checkVarRegex(t, locals, -1, "m1", "m1", m1Truncated, `map\[string\]main\.astruct`, true)
})
}
},
disconnect: true,
}})