diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go index 2b897241..8419f1c5 100644 --- a/service/dap/daptest/client.go +++ b/service/dap/daptest/client.go @@ -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)) diff --git a/service/dap/server.go b/service/dap/server.go index 6dcbd4e8..be5913a9 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -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) diff --git a/service/dap/server_test.go b/service/dap/server_test.go index bdf43135..10e21a6b 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -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, }})