dap: implement variable references
Implement variable references to inspect the state of a stack frame. Variable reference ids are composed of two sections. A thread mask that is the first 8 bytes and the remainder is an increasing number that gets reset each time a thread is resumed. This allows the adapter to know which thread to delegate the variables request to and allows the variable references to still remain confined to each thread. An int32 is used for this because variable references need to be in the range of (0, 2^32). At the moment, only the platform variables and some of the exec operations for an operation. These are labeled as "arguments" to the stack frame. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
@@ -216,6 +216,7 @@ func (d *Adapter[C]) newThread(ctx Context, name string) (t *thread) {
|
|||||||
sourceMap: &d.sourceMap,
|
sourceMap: &d.sourceMap,
|
||||||
breakpointMap: d.breakpointMap,
|
breakpointMap: d.breakpointMap,
|
||||||
idPool: d.idPool,
|
idPool: d.idPool,
|
||||||
|
variables: newVariableReferences(),
|
||||||
}
|
}
|
||||||
d.threads[t.id] = t
|
d.threads[t.id] = t
|
||||||
d.nextThreadID++
|
d.nextThreadID++
|
||||||
@@ -240,6 +241,11 @@ func (d *Adapter[C]) getThread(id int) (t *thread) {
|
|||||||
|
|
||||||
func (d *Adapter[C]) deleteThread(ctx Context, t *thread) {
|
func (d *Adapter[C]) deleteThread(ctx Context, t *thread) {
|
||||||
d.threadsMu.Lock()
|
d.threadsMu.Lock()
|
||||||
|
if t := d.threads[t.id]; t != nil {
|
||||||
|
if t.variables != nil {
|
||||||
|
t.variables.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
delete(d.threads, t.id)
|
delete(d.threads, t.id)
|
||||||
d.threadsMu.Unlock()
|
d.threadsMu.Unlock()
|
||||||
|
|
||||||
@@ -252,6 +258,18 @@ func (d *Adapter[C]) deleteThread(ctx Context, t *thread) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Adapter[T]) getThreadByFrameID(id int) (t *thread) {
|
||||||
|
d.threadsMu.RLock()
|
||||||
|
defer d.threadsMu.RUnlock()
|
||||||
|
|
||||||
|
for _, t := range d.threads {
|
||||||
|
if t.hasFrame(id) {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type evaluateRequest struct {
|
type evaluateRequest struct {
|
||||||
name string
|
name string
|
||||||
c gateway.Client
|
c gateway.Client
|
||||||
@@ -330,6 +348,35 @@ func (d *Adapter[C]) StackTrace(c Context, req *dap.StackTraceRequest, resp *dap
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Adapter[C]) Scopes(c Context, req *dap.ScopesRequest, resp *dap.ScopesResponse) error {
|
||||||
|
t := d.getThreadByFrameID(req.Arguments.FrameId)
|
||||||
|
if t == nil {
|
||||||
|
return errors.Errorf("no such frame id: %d", req.Arguments.FrameId)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Body.Scopes = t.Scopes(req.Arguments.FrameId)
|
||||||
|
for i, s := range resp.Body.Scopes {
|
||||||
|
resp.Body.Scopes[i].VariablesReference = (t.id << 24) | s.VariablesReference
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Adapter[C]) Variables(c Context, req *dap.VariablesRequest, resp *dap.VariablesResponse) error {
|
||||||
|
tid := req.Arguments.VariablesReference >> 24
|
||||||
|
|
||||||
|
t := d.getThread(tid)
|
||||||
|
if t == nil {
|
||||||
|
return errors.Errorf("no such thread: %d", tid)
|
||||||
|
}
|
||||||
|
|
||||||
|
varRef := req.Arguments.VariablesReference & ((1 << 24) - 1)
|
||||||
|
resp.Body.Variables = t.Variables(varRef)
|
||||||
|
for i, ref := range resp.Body.Variables {
|
||||||
|
resp.Body.Variables[i].VariablesReference = (tid << 24) | ref.VariablesReference
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Adapter[C]) Source(c Context, req *dap.SourceRequest, resp *dap.SourceResponse) error {
|
func (d *Adapter[C]) Source(c Context, req *dap.SourceRequest, resp *dap.SourceResponse) error {
|
||||||
fname := req.Arguments.Source.Path
|
fname := req.Arguments.Source.Path
|
||||||
|
|
||||||
@@ -378,6 +425,8 @@ func (d *Adapter[C]) dapHandler() Handler {
|
|||||||
Disconnect: d.Disconnect,
|
Disconnect: d.Disconnect,
|
||||||
Threads: d.Threads,
|
Threads: d.Threads,
|
||||||
StackTrace: d.StackTrace,
|
StackTrace: d.StackTrace,
|
||||||
|
Scopes: d.Scopes,
|
||||||
|
Variables: d.Variables,
|
||||||
Source: d.Source,
|
Source: d.Source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ type Handler struct {
|
|||||||
Restart HandlerFunc[*dap.RestartRequest, *dap.RestartResponse]
|
Restart HandlerFunc[*dap.RestartRequest, *dap.RestartResponse]
|
||||||
Threads HandlerFunc[*dap.ThreadsRequest, *dap.ThreadsResponse]
|
Threads HandlerFunc[*dap.ThreadsRequest, *dap.ThreadsResponse]
|
||||||
StackTrace HandlerFunc[*dap.StackTraceRequest, *dap.StackTraceResponse]
|
StackTrace HandlerFunc[*dap.StackTraceRequest, *dap.StackTraceResponse]
|
||||||
|
Scopes HandlerFunc[*dap.ScopesRequest, *dap.ScopesResponse]
|
||||||
|
Variables HandlerFunc[*dap.VariablesRequest, *dap.VariablesResponse]
|
||||||
Evaluate HandlerFunc[*dap.EvaluateRequest, *dap.EvaluateResponse]
|
Evaluate HandlerFunc[*dap.EvaluateRequest, *dap.EvaluateResponse]
|
||||||
Source HandlerFunc[*dap.SourceRequest, *dap.SourceResponse]
|
Source HandlerFunc[*dap.SourceRequest, *dap.SourceResponse]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,10 @@ func (s *Server) handleMessage(c Context, m dap.Message) (dap.ResponseMessage, e
|
|||||||
return s.h.Threads.Do(c, req)
|
return s.h.Threads.Do(c, req)
|
||||||
case *dap.StackTraceRequest:
|
case *dap.StackTraceRequest:
|
||||||
return s.h.StackTrace.Do(c, req)
|
return s.h.StackTrace.Do(c, req)
|
||||||
|
case *dap.ScopesRequest:
|
||||||
|
return s.h.Scopes.Do(c, req)
|
||||||
|
case *dap.VariablesRequest:
|
||||||
|
return s.h.Variables.Do(c, req)
|
||||||
case *dap.EvaluateRequest:
|
case *dap.EvaluateRequest:
|
||||||
return s.h.Evaluate.Do(c, req)
|
return s.h.Evaluate.Do(c, req)
|
||||||
case *dap.SourceRequest:
|
case *dap.SourceRequest:
|
||||||
|
|||||||
+50
-53
@@ -26,6 +26,7 @@ type thread struct {
|
|||||||
idPool *idPool
|
idPool *idPool
|
||||||
sourceMap *sourceMap
|
sourceMap *sourceMap
|
||||||
breakpointMap *breakpointMap
|
breakpointMap *breakpointMap
|
||||||
|
variables *variableReferences
|
||||||
|
|
||||||
// Inputs to the evaluate call.
|
// Inputs to the evaluate call.
|
||||||
c gateway.Client
|
c gateway.Client
|
||||||
@@ -48,11 +49,10 @@ type thread struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
// Attributes set when a thread is paused.
|
// Attributes set when a thread is paused.
|
||||||
rCtx *build.ResultHandle
|
rCtx *build.ResultHandle
|
||||||
curPos digest.Digest
|
curPos digest.Digest
|
||||||
|
stackTrace []int32
|
||||||
// Lazy attributes that are set when a thread is paused.
|
frames map[int32]*frame
|
||||||
stackTrace []dap.StackFrame
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type region struct {
|
type region struct {
|
||||||
@@ -154,6 +154,7 @@ func (t *thread) pause(c Context, err error, event dap.StoppedEventBody) <-chan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
t.collectStackTrace()
|
||||||
|
|
||||||
event.ThreadId = t.id
|
event.ThreadId = t.id
|
||||||
c.C() <- &dap.StoppedEvent{
|
c.C() <- &dap.StoppedEvent{
|
||||||
@@ -178,18 +179,7 @@ func (t *thread) resume(step stepType) {
|
|||||||
if t.paused == nil {
|
if t.paused == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
t.releaseState()
|
||||||
if t.rCtx != nil {
|
|
||||||
t.rCtx.Done()
|
|
||||||
t.rCtx = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.stackTrace != nil {
|
|
||||||
for _, frame := range t.stackTrace {
|
|
||||||
t.idPool.Put(int64(frame.Id))
|
|
||||||
}
|
|
||||||
t.stackTrace = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
t.paused <- step
|
t.paused <- step
|
||||||
close(t.paused)
|
close(t.paused)
|
||||||
@@ -207,10 +197,23 @@ func (t *thread) StackTrace() []dap.StackFrame {
|
|||||||
return []dap.StackFrame{}
|
return []dap.StackFrame{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.stackTrace == nil {
|
frames := make([]dap.StackFrame, len(t.stackTrace))
|
||||||
t.stackTrace = t.makeStackTrace()
|
for i, id := range t.stackTrace {
|
||||||
|
frames[i] = t.frames[id].StackFrame
|
||||||
}
|
}
|
||||||
return t.stackTrace
|
return frames
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *thread) Scopes(frameID int) []dap.Scope {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
frame := t.frames[int32(frameID)]
|
||||||
|
return frame.Scopes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *thread) Variables(id int) []dap.Variable {
|
||||||
|
return t.variables.Get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *thread) getLLBState(ctx Context) error {
|
func (t *thread) getLLBState(ctx Context) error {
|
||||||
@@ -502,15 +505,16 @@ func (t *thread) solve(ctx context.Context, target digest.Digest) (gateway.Refer
|
|||||||
return res.SingleRef()
|
return res.SingleRef()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *thread) newStackFrame() dap.StackFrame {
|
func (t *thread) releaseState() {
|
||||||
return dap.StackFrame{
|
if t.rCtx != nil {
|
||||||
Id: int(t.idPool.Get()),
|
t.rCtx.Done()
|
||||||
|
t.rCtx = nil
|
||||||
}
|
}
|
||||||
|
t.stackTrace = nil
|
||||||
|
t.frames = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *thread) makeStackTrace() []dap.StackFrame {
|
func (t *thread) collectStackTrace() {
|
||||||
var frames []dap.StackFrame
|
|
||||||
|
|
||||||
region := t.regionsByDigest[t.curPos]
|
region := t.regionsByDigest[t.curPos]
|
||||||
r := t.regions[region]
|
r := t.regions[region]
|
||||||
|
|
||||||
@@ -519,45 +523,38 @@ func (t *thread) makeStackTrace() []dap.StackFrame {
|
|||||||
digests = digests[:index+1]
|
digests = digests[:index+1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.frames = make(map[int32]*frame)
|
||||||
for i := len(digests) - 1; i >= 0; i-- {
|
for i := len(digests) - 1; i >= 0; i-- {
|
||||||
dgst := digests[i]
|
dgst := digests[i]
|
||||||
|
|
||||||
frame := t.newStackFrame()
|
frame := &frame{}
|
||||||
|
frame.Id = int(t.idPool.Get())
|
||||||
|
|
||||||
if meta, ok := t.def.Metadata[dgst]; ok {
|
if meta, ok := t.def.Metadata[dgst]; ok {
|
||||||
fillStackFrameMetadata(&frame, meta)
|
frame.setNameFromMeta(meta)
|
||||||
}
|
}
|
||||||
if loc, ok := t.def.Source.Locations[string(dgst)]; ok {
|
if loc, ok := t.def.Source.Locations[string(dgst)]; ok {
|
||||||
t.fillStackFrameLocation(&frame, loc)
|
frame.fillLocation(t.def, loc, t.sourcePath)
|
||||||
}
|
}
|
||||||
frames = append(frames, frame)
|
|
||||||
|
if op := t.ops[dgst]; op != nil {
|
||||||
|
frame.fillVarsFromOp(op, t.variables)
|
||||||
|
}
|
||||||
|
t.stackTrace = append(t.stackTrace, int32(frame.Id))
|
||||||
|
t.frames[int32(frame.Id)] = frame
|
||||||
}
|
}
|
||||||
return frames
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fillStackFrameMetadata(frame *dap.StackFrame, meta llb.OpMetadata) {
|
func (t *thread) hasFrame(id int) bool {
|
||||||
if name, ok := meta.Description["llb.customname"]; ok {
|
t.mu.Lock()
|
||||||
frame.Name = name
|
defer t.mu.Unlock()
|
||||||
} else if cmd, ok := meta.Description["com.docker.dockerfile.v1.command"]; ok {
|
|
||||||
frame.Name = cmd
|
|
||||||
}
|
|
||||||
// TODO: should we infer the name from somewhere else?
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *thread) fillStackFrameLocation(frame *dap.StackFrame, loc *pb.Locations) {
|
if t.paused == nil {
|
||||||
for _, l := range loc.Locations {
|
return false
|
||||||
for _, r := range l.Ranges {
|
|
||||||
frame.Line = int(r.Start.Line)
|
|
||||||
frame.Column = int(r.Start.Character)
|
|
||||||
frame.EndLine = int(r.End.Line)
|
|
||||||
frame.EndColumn = int(r.End.Character)
|
|
||||||
|
|
||||||
info := t.def.Source.Infos[l.SourceIndex]
|
|
||||||
frame.Source = &dap.Source{
|
|
||||||
Path: filepath.Join(t.sourcePath, info.Filename),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, ok := t.frames[int32(id)]
|
||||||
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func pop[S ~[]E, E any](s *S) E {
|
func pop[S ~[]E, E any](s *S) E {
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package dap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/google/go-dap"
|
||||||
|
"github.com/moby/buildkit/client/llb"
|
||||||
|
"github.com/moby/buildkit/solver/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type frame struct {
|
||||||
|
dap.StackFrame
|
||||||
|
scopes []dap.Scope
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frame) setNameFromMeta(meta llb.OpMetadata) {
|
||||||
|
if name, ok := meta.Description["llb.customname"]; ok {
|
||||||
|
f.Name = name
|
||||||
|
} else if cmd, ok := meta.Description["com.docker.dockerfile.v1.command"]; ok {
|
||||||
|
f.Name = cmd
|
||||||
|
}
|
||||||
|
// TODO: should we infer the name from somewhere else?
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frame) fillLocation(def *llb.Definition, loc *pb.Locations, ws string) {
|
||||||
|
for _, l := range loc.Locations {
|
||||||
|
for _, r := range l.Ranges {
|
||||||
|
f.Line = int(r.Start.Line)
|
||||||
|
f.Column = int(r.Start.Character)
|
||||||
|
f.EndLine = int(r.End.Line)
|
||||||
|
f.EndColumn = int(r.End.Character)
|
||||||
|
|
||||||
|
info := def.Source.Infos[l.SourceIndex]
|
||||||
|
f.Source = &dap.Source{
|
||||||
|
Path: filepath.Join(ws, info.Filename),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frame) fillVarsFromOp(op *pb.Op, refs *variableReferences) {
|
||||||
|
f.scopes = []dap.Scope{
|
||||||
|
{
|
||||||
|
Name: "Arguments",
|
||||||
|
PresentationHint: "arguments",
|
||||||
|
VariablesReference: refs.New(func() []dap.Variable {
|
||||||
|
var vars []dap.Variable
|
||||||
|
if op.Platform != nil {
|
||||||
|
vars = append(vars, platformVars(op.Platform, refs))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch op := op.Op.(type) {
|
||||||
|
case *pb.Op_Exec:
|
||||||
|
vars = append(vars, execOpVars(op.Exec, refs))
|
||||||
|
}
|
||||||
|
return vars
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func platformVars(platform *pb.Platform, refs *variableReferences) dap.Variable {
|
||||||
|
return dap.Variable{
|
||||||
|
Name: "platform",
|
||||||
|
Value: fmt.Sprintf("%s/%s", platform.OS, platform.Architecture),
|
||||||
|
VariablesReference: refs.New(func() []dap.Variable {
|
||||||
|
vars := []dap.Variable{
|
||||||
|
{
|
||||||
|
Name: "architecture",
|
||||||
|
Value: platform.Architecture,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "os",
|
||||||
|
Value: platform.OS,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if platform.Variant != "" {
|
||||||
|
vars = append(vars, dap.Variable{
|
||||||
|
Name: "variant",
|
||||||
|
Value: platform.Variant,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if platform.OSVersion != "" {
|
||||||
|
vars = append(vars, dap.Variable{
|
||||||
|
Name: "osversion",
|
||||||
|
Value: platform.OSVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return vars
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func execOpVars(exec *pb.ExecOp, refs *variableReferences) dap.Variable {
|
||||||
|
return dap.Variable{
|
||||||
|
Name: "exec",
|
||||||
|
Value: strings.Join(exec.Meta.Args, " "),
|
||||||
|
VariablesReference: refs.New(func() []dap.Variable {
|
||||||
|
vars := []dap.Variable{
|
||||||
|
{
|
||||||
|
Name: "args",
|
||||||
|
Value: brief(strings.Join(exec.Meta.Args, " ")),
|
||||||
|
VariablesReference: refs.New(func() []dap.Variable {
|
||||||
|
vars := make([]dap.Variable, 0, len(exec.Meta.Args))
|
||||||
|
for i, arg := range exec.Meta.Args {
|
||||||
|
vars = append(vars, dap.Variable{
|
||||||
|
Name: strconv.Itoa(i),
|
||||||
|
Value: arg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return vars
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "env",
|
||||||
|
Value: brief(strings.Join(exec.Meta.Env, " ")),
|
||||||
|
VariablesReference: refs.New(func() []dap.Variable {
|
||||||
|
vars := make([]dap.Variable, 0, len(exec.Meta.Env))
|
||||||
|
for _, envstr := range exec.Meta.Env {
|
||||||
|
parts := strings.SplitN(envstr, "=", 2)
|
||||||
|
vars = append(vars, dap.Variable{
|
||||||
|
Name: parts[0],
|
||||||
|
Value: parts[1],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return vars
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if exec.Meta.Cwd != "" {
|
||||||
|
vars = append(vars, dap.Variable{
|
||||||
|
Name: "workdir",
|
||||||
|
Value: exec.Meta.Cwd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if exec.Meta.User != "" {
|
||||||
|
vars = append(vars, dap.Variable{
|
||||||
|
Name: "user",
|
||||||
|
Value: exec.Meta.User,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return vars
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frame) Scopes() []dap.Scope {
|
||||||
|
return f.scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
type variableReferences struct {
|
||||||
|
refs map[int32]func() []dap.Variable
|
||||||
|
nextID atomic.Int32
|
||||||
|
mask int32
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newVariableReferences() *variableReferences {
|
||||||
|
v := new(variableReferences)
|
||||||
|
v.Reset()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *variableReferences) New(fn func() []dap.Variable) int {
|
||||||
|
v.mu.Lock()
|
||||||
|
defer v.mu.Unlock()
|
||||||
|
|
||||||
|
id := v.nextID.Add(1) | v.mask
|
||||||
|
v.refs[id] = sync.OnceValue(fn)
|
||||||
|
return int(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *variableReferences) Get(id int) []dap.Variable {
|
||||||
|
v.mu.RLock()
|
||||||
|
fn := v.refs[int32(id)]
|
||||||
|
v.mu.RUnlock()
|
||||||
|
|
||||||
|
var vars []dap.Variable
|
||||||
|
if fn != nil {
|
||||||
|
vars = fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
if vars == nil {
|
||||||
|
vars = []dap.Variable{}
|
||||||
|
}
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *variableReferences) Reset() {
|
||||||
|
v.mu.Lock()
|
||||||
|
defer v.mu.Unlock()
|
||||||
|
|
||||||
|
v.refs = make(map[int32]func() []dap.Variable)
|
||||||
|
v.nextID.Store(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func brief(s string) string {
|
||||||
|
if len(s) >= 64 {
|
||||||
|
return s[:60] + " ..."
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user