Skip to content

Commit

Permalink
Share variable inspection logic between CDP and DAP
Browse files Browse the repository at this point in the history
  • Loading branch information
amomchilov committed Jul 28, 2023
1 parent 4ec9d7a commit de6411b
Show file tree
Hide file tree
Showing 7 changed files with 486 additions and 182 deletions.
52 changes: 52 additions & 0 deletions lib/debug/limited_pp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require "pp"

module DEBUGGER__
class LimitedPP
SHORT_INSPECT_LENGTH = 40

def self.pp(obj, max = 80)
out = self.new(max)
catch out do
::PP.singleline_pp(obj, out)
end
out.buf
end

attr_reader :buf

def initialize max
@max = max
@cnt = 0
@buf = String.new
end

def <<(other)
@buf << other

if @buf.size >= @max
@buf = @buf[0..@max] + '...'
throw self
end
end

def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false
if short
LimitedPP.pp(obj, max_length)
else
obj.inspect
end
rescue NoMethodError => e
klass, oid = M_CLASS.bind_call(obj), M_OBJECT_ID.bind_call(obj)
if obj == (r = e.receiver)
"<\##{klass.name}#{oid} does not have \#inspect>"
else
rklass, roid = M_CLASS.bind_call(r), M_OBJECT_ID.bind_call(r)
"<\##{klass.name}:#{roid} contains <\##{rklass}:#{roid} and it does not have #inspect>"
end
rescue Exception => e
"<#inspect raises #{e.inspect}>"
end
end
end
55 changes: 20 additions & 35 deletions lib/debug/server_cdp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
require 'tmpdir'
require 'tempfile'
require 'timeout'
require_relative 'variable'
require_relative 'variable_inspector'

module DEBUGGER__
module UI_CDP
Expand Down Expand Up @@ -1112,46 +1114,29 @@ def process_cdp args
event! :protocol_result, :scope, req, vars
when :properties
oid = args.shift
result = []
prop = []

if obj = @obj_map[oid]
case obj
when Array
result = obj.map.with_index{|o, i|
variable i.to_s, o
}
when Hash
result = obj.map{|k, v|
variable(k, v)
}
when Struct
result = obj.members.map{|m|
variable(m, obj[m])
}
when String
prop = [
internalProperty('#length', obj.length),
internalProperty('#encoding', obj.encoding)
]
when Class, Module
result = obj.instance_variables.map{|iv|
variable(iv, obj.instance_variable_get(iv))
}
prop = [internalProperty('%ancestors', obj.ancestors[1..])]
when Range
prop = [
internalProperty('#begin', obj.begin),
internalProperty('#end', obj.end),
]
members = if Array === obj
VariableInspector.new.indexed_members_of(obj, start: 0, count: obj.size)
else
VariableInspector.new.named_members_of(obj)
end

result += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
}
prop += [internalProperty('#class', M_CLASS.bind_call(obj))]
result = members.filter_map do |member|
next if member.internal?
variable(member.name, member.value)
end

internal_properties = members.filter_map do |member|
next unless member.internal?
internalProperty(member.name, member.value)
end
else
result = []
internal_properties = []
end
event! :protocol_result, :properties, req, result: result, internalProperties: prop

event! :protocol_result, :properties, req, result: result, internalProperties: internal_properties
when :exception
oid = args.shift
exc = nil
Expand Down
155 changes: 55 additions & 100 deletions lib/debug/server_dap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
require 'irb/completion'
require 'tmpdir'
require 'fileutils'
require_relative 'variable'
require_relative 'variable_inspector'

module DEBUGGER__
module UI_DAP
Expand Down Expand Up @@ -765,18 +767,11 @@ def register_vars vars, tid
end
end

class NaiveString
attr_reader :str
def initialize str
@str = str
end
end

class ThreadClient
MAX_LENGTH = 180

def value_inspect obj, short: true
# TODO: max length should be configuarable?
# TODO: max length should be configurable?
str = DEBUGGER__.safe_inspect obj, short: short, max_length: MAX_LENGTH

if str.encoding == Encoding::UTF_8
Expand Down Expand Up @@ -867,56 +862,28 @@ def process_dap args
fid = args.shift
frame = get_frame(fid)
vars = collect_locals(frame).map do |var, val|
variable(var, val)
render_variable Variable.new(name: var, value: val)
end

event! :protocol_result, :scope, req, variables: vars, tid: self.id
when :variable
vid = args.shift
obj = @var_map[vid]
if obj
case req.dig('arguments', 'filter')
when 'indexed'
start = req.dig('arguments', 'start') || 0
count = req.dig('arguments', 'count') || obj.size
vars = (start ... (start + count)).map{|i|
variable(i.to_s, obj[i])
}
else
vars = []

case obj
when Hash
vars = obj.map{|k, v|
variable(value_inspect(k), v,)
}
when Struct
vars = obj.members.map{|m|
variable(m, obj[m])
}
when String
vars = [
variable('#length', obj.length),
variable('#encoding', obj.encoding),
]
printed_str = value_inspect(obj)
vars << variable('#dump', NaiveString.new(obj)) if printed_str.end_with?('...')
when Class, Module
vars << variable('%ancestors', obj.ancestors[1..])
when Range
vars = [
variable('#begin', obj.begin),
variable('#end', obj.end),
]
end
if @var_map.has_key?(vid)
obj = @var_map[vid]

unless NaiveString === obj
vars += M_INSTANCE_VARIABLES.bind_call(obj).sort.map{|iv|
variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
}
vars.unshift variable('#class', M_CLASS.bind_call(obj))
end
members = case req.dig('arguments', 'filter')
when 'indexed'
VariableInspector.new.indexed_members_of(
obj,
start: req.dig('arguments', 'start') || 0,
count: req.dig('arguments', 'count') || obj.size,
)
else
VariableInspector.new.named_members_of(obj)
end

vars = members.map { |member| render_variable member }
end
event! :protocol_result, :variable, req, variables: (vars || []), tid: self.id

Expand Down Expand Up @@ -973,7 +940,13 @@ def process_dap args
result = 'Error: Can not evaluate on this frame'
end

event! :protocol_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result)
result_variable = Variable.new(name: nil, value: result)

event! :protocol_result, :evaluate, req,
message: message,
tid: self.id,
result: result_variable.inspect_value,
**render_variable(result_variable)

when :completions
fid, text = args
Expand Down Expand Up @@ -1035,72 +1008,54 @@ def search_const b, expr
false
end

def evaluate_result r
variable nil, r
end
# Renders the given Member into a DAP Variable
# https://microsoft.github.io/debug-adapter-protocol/specification#variable
def render_variable member
indexedVariables, namedVariables = if Array === member.value
[member.value.size, 0]
else
[0, VariableInspector.new.named_members_of(member.value).count]
end

def type_name obj
klass = M_CLASS.bind_call(obj)

begin
M_NAME.bind_call(klass) || klass.to_s
rescue Exception => e
"<Error: #{e.message} (#{e.backtrace.first}>"
if member.value == false || member.value == true
require "awesome_print"
ap({ member:, indexedVariables:, namedVariables: })
end
end

def variable_ name, obj, indexedVariables: 0, namedVariables: 0
# > If `variablesReference` is > 0, the variable is structured and its children
# > can be retrieved by passing `variablesReference` to the `variables` request
# > as long as execution remains suspended.
if indexedVariables > 0 || namedVariables > 0
# This object has children that we might need to query, so we need to remember it by its vid
vid = @var_map.size + 1
@var_map[vid] = obj
@var_map[vid] = member.value
else
# This object has no children, so we don't need to remember it in the `@var_map`
vid = 0
end

namedVariables += M_INSTANCE_VARIABLES.bind_call(obj).size

if NaiveString === obj
str = obj.str.dump
vid = indexedVariables = namedVariables = 0
else
str = value_inspect(obj)
end

if name
{ name: name,
value: str,
type: type_name(obj),
variable = if member.name
# These two hashes are repeated so the "name" can come always come first, when available,
# which improves the readability of protocol responses.
{
name: member.name,
value: member.inspect_value,
type: member.value_type_name,
variablesReference: vid,
indexedVariables: indexedVariables,
namedVariables: namedVariables,
}
else
{ result: str,
type: type_name(obj),
{
value: member.inspect_value,
type: member.value_type_name,
variablesReference: vid,
indexedVariables: indexedVariables,
namedVariables: namedVariables,
}
end
end

def variable name, obj
case obj
when Array
variable_ name, obj, indexedVariables: obj.size
when Hash
variable_ name, obj, namedVariables: obj.size
when String
variable_ name, obj, namedVariables: 3 # #length, #encoding, #to_str
when Struct
variable_ name, obj, namedVariables: obj.size
when Class, Module
variable_ name, obj, namedVariables: 1 # %ancestors (#ancestors without self)
when Range
variable_ name, obj, namedVariables: 2 # #begin, #end
else
variable_ name, obj, namedVariables: 1 # #class
end
variable[:indexedVariables] = indexedVariables unless indexedVariables == 0
variable[:namedVariables] = namedVariables unless namedVariables == 0

variable
end
end
end
Loading

0 comments on commit de6411b

Please sign in to comment.