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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 28 deletions

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

@ -688,6 +688,7 @@ func (s *Server) onInitializeRequest(request *dap.InitializeRequest) {
response.Body.SupportsExceptionInfoRequest = true response.Body.SupportsExceptionInfoRequest = true
response.Body.SupportsSetVariable = true response.Body.SupportsSetVariable = true
response.Body.SupportsEvaluateForHovers = true response.Body.SupportsEvaluateForHovers = true
response.Body.SupportsClipboardContext = true
// TODO(polina): support these requests in addition to vscode-go feature parity // TODO(polina): support these requests in addition to vscode-go feature parity
response.Body.SupportsTerminateRequest = false response.Body.SupportsTerminateRequest = false
response.Body.SupportsRestartRequest = 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 // 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). // 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) { 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 { func (s *Server) convertVariableToString(v *proc.Variable) string {
val, _ := s.convertVariableWithOpts(v, "", true) val, _ := s.convertVariableWithOpts(v, "", skipRef)
return val 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 // convertVariableWithOpts allows to skip reference generation in case all we need is
// a string representation of the variable. // a string representation of the variable. When the variable is a compound or reference
func (s *Server) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr string, skipRef bool) (value string, variablesReference int) { // 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 { maybeCreateVariableHandle := func(v *proc.Variable) int {
if skipRef { canHaveRef = true
if opts&skipRef != 0 {
return 0 return 0
} }
return s.variableHandles.create(&fullyQualifiedVariable{v, qualifiedNameOrExpr, false /*not a scope*/}) return s.variableHandles.create(&fullyQualifiedVariable{v, qualifiedNameOrExpr, false /*not a scope*/})
} }
value = api.ConvertVar(v).SinglelineString() value = api.ConvertVar(v).SinglelineString()
if v.Unreadable != nil { if v.Unreadable != nil {
return value, variablesReference return value, 0
} }
// Some of the types might be fully or partially not loaded based on LoadConfig. // 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) variablesReference = maybeCreateVariableHandle(v)
} }
} }
canTruncateValue := showFullValue&opts == 0
if len(value) > defaultMaxValueLen && canTruncateValue && canHaveRef {
value = value[:defaultMaxValueLen] + "..."
}
return value, variablesReference return value, variablesReference
} }
@ -2019,21 +2040,27 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
return return
} }
if exprVar.Kind == reflect.String && (request.Arguments.Context == "repl" || request.Arguments.Context == "variables") { ctxt := request.Arguments.Context
if strVal := constant.StringVal(exprVar.Value); exprVar.Len > int64(len(strVal)) { switch ctxt {
// reload string value with a bigger limit. case "repl", "variables", "hover", "clipboard":
loadCfg := DefaultLoadConfig if exprVar.Kind == reflect.String {
loadCfg.MaxStringLen = 4 << 10 if strVal := constant.StringVal(exprVar.Value); exprVar.Len > int64(len(strVal)) {
if v, err := s.debugger.EvalVariableInScope(goid, frame, 0, request.Arguments.Expression, loadCfg); err != nil { // reload string value with a bigger limit.
s.log.Debugf("Failed to load more for %v: %v", request.Arguments.Expression, err) loadCfg := DefaultLoadConfig
} else { loadCfg.MaxStringLen = 4 << 10
exprVar = v 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. var opts convertVariableFlags
// Should it be skipped altogether for all levels? if ctxt == "variables" || ctxt == "hover" || ctxt == "clipboard" {
exprVal, exprRef := s.convertVariable(exprVar, fmt.Sprintf("(%s)", request.Arguments.Expression)) opts |= showFullValue
}
exprVal, exprRef := s.convertVariableWithOpts(exprVar, fmt.Sprintf("(%s)", request.Arguments.Expression), opts)
response.Body = dap.EvaluateResponseBody{Result: exprVal, VariablesReference: exprRef} response.Body = dap.EvaluateResponseBody{Result: exprVal, VariablesReference: exprRef}
} }
s.send(response) s.send(response)

@ -1493,7 +1493,7 @@ func TestVariablesLoading(t *testing.T) {
} }
// Map partially missing based on LoadConfig.MaxArrayValues // 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 { if ref > 0 {
client.VariablesRequest(ref) client.VariablesRequest(ref)
m1 := client.ExpectVariablesResponse(t) m1 := client.ExpectVariablesResponse(t)
@ -1572,7 +1572,7 @@ func TestVariablesLoading(t *testing.T) {
client.VariablesRequest(ref) client.VariablesRequest(ref)
tmV := client.ExpectVariablesResponse(t) tmV := client.ExpectVariablesResponse(t)
checkChildren(t, tmV, "tm.v", 1) 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 { if ref > 0 {
client.VariablesRequest(ref) client.VariablesRequest(ref)
tmV0 := client.ExpectVariablesResponse(t) tmV0 := client.ExpectVariablesResponse(t)
@ -1602,7 +1602,7 @@ func TestVariablesLoading(t *testing.T) {
tmV := client.ExpectVariablesResponse(t) tmV := client.ExpectVariablesResponse(t)
checkChildren(t, tmV, "tm.v", 1) checkChildren(t, tmV, "tm.v", 1)
// TODO(polina): this evaluate name is not usable - it should be empty // 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 { if ref > 0 {
client.VariablesRequest(ref) client.VariablesRequest(ref)
tmV0 := client.ExpectVariablesResponse(t) 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" { 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) 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, disconnect: false,
}}) }})
}) })
} }
func TestEvaluateRequestLongStr(t *testing.T) { func TestEvaluateRequestLongStrLargeValue(t *testing.T) {
runTest(t, "testvariables2", func(client *daptest.Client, fixture protest.Fixture) { runTest(t, "testvariables2", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client, "launch", runDebugSessionWithBPs(t, client, "launch",
// Launch // Launch
@ -2741,22 +2746,24 @@ func TestEvaluateRequestLongStr(t *testing.T) {
[]onBreakpoint{{ []onBreakpoint{{
execute: func() { execute: func() {
longstr := `"very long string 0123456789a0123456789b0123456789c0123456789d0123456789e0123456789f0123456789g012345678h90123456789i0123456789j0123456789"`
longstrTruncated := `"very long string 0123456789a0123456789b0123456789c0123456789d012...+73 more"`
checkStop(t, client, 1, "main.main", -1) checkStop(t, client, 1, "main.main", -1)
client.VariablesRequest(1001) // Locals client.VariablesRequest(1001) // Locals
locals := client.ExpectVariablesResponse(t) locals := client.ExpectVariablesResponse(t)
// reflect.Kind == String, load with longer load limit if evaluated in repl/variables context. // 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) { t.Run(evalContext, func(t *testing.T) {
// long string // long string
longstr := `"very long string 0123456789a0123456789b0123456789c0123456789d0123456789e0123456789f0123456789g012345678h90123456789i0123456789j0123456789"`
longstrTruncated := `"very long string 0123456789a0123456789b0123456789c0123456789d012...+73 more"`
client.EvaluateRequest("longstr", 0, evalContext) client.EvaluateRequest("longstr", 0, evalContext)
got := client.ExpectEvaluateResponse(t) got := client.ExpectEvaluateResponse(t)
want := longstrTruncated want := longstrTruncated
if evalContext == "repl" || evalContext == "variables" { switch evalContext {
case "repl", "variables", "hover", "clipboard":
want = longstr want = longstr
} }
checkEval(t, got, want, false) checkEval(t, got, want, false)
@ -2769,9 +2776,22 @@ func TestEvaluateRequestLongStr(t *testing.T) {
// variables are not affected. // variables are not affected.
checkVarExact(t, locals, -1, "longstr", "longstr", longstrTruncated, "string", noChildren) checkVarExact(t, locals, -1, "longstr", "longstr", longstrTruncated, "string", noChildren)
checkVarExact(t, locals, -1, "m6", "m6", `main.C {s: `+longstrTruncated+`}`, "main.C", hasChildren) 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, disconnect: true,
}}) }})