Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 146
Share variable inspection logic between CDP and DAP#1001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base:master
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
75da7ca126b6c223b23f80495633cf7e379a14c540c790c31File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -8,6 +8,7 @@ | ||
| require 'tmpdir' | ||
| require 'tempfile' | ||
| require 'timeout' | ||
| require_relative 'variable_inspector' | ||
| module DEBUGGER__ | ||
| module UI_CDP | ||
| @@ -1111,46 +1112,31 @@ 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), | ||
| ] | ||
| if @obj_map.key?(oid) | ||
| obj = @obj_map[oid] | ||
| 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))] | ||
Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For Chrome, | ||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -4,6 +4,7 @@ | ||
| require 'irb/completion' | ||
| require 'tmpdir' | ||
| require 'fileutils' | ||
| require_relative 'variable_inspector' | ||
| module DEBUGGER__ | ||
| module UI_DAP | ||
| @@ -765,18 +766,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 | ||
| @@ -855,70 +849,40 @@ def process_dap args | ||
| presentationHint: 'locals', | ||
| # variablesReference: N, # filled by SESSION | ||
| namedVariables: lnum, | ||
| indexedVariables: 0, | ||
| expensive: false, | ||
| }, { | ||
| name: 'Global variables', | ||
| presentationHint: 'globals', | ||
| variablesReference: 1, # GLOBAL | ||
| namedVariables: safe_global_variables.size, | ||
| indexedVariables: 0, | ||
| expensive: false, | ||
| }] | ||
| when :scope | ||
| 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 | ||
Comment on lines
-878
to
-879
Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This had a small bug, where ![]() Notice that The new check fixes it: ![]()
| ||
| 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.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 | ||
| @@ -975,7 +939,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) | ||
Comment on lines
+947
to
+948
Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This | ||
| when :completions | ||
| fid, text = args | ||
| @@ -1051,58 +1021,49 @@ def type_name obj | ||
| end | ||
| end | ||
| def variable_ name, obj, indexedVariables: 0, namedVariables: 0 | ||
| if indexedVariables > 0 || namedVariables > 0 | ||
| vid = @var_map.size + 1 | ||
| @var_map[vid] = obj | ||
| # 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 | ||
| vid = 0 | ||
| [0, VariableInspector.new.named_members_of(member.value).count] | ||
| end | ||
| namedVariables += M_INSTANCE_VARIABLES.bind_call(obj).size | ||
| if NaiveString === obj | ||
| str = obj.str.dump | ||
| vid = indexedVariables = 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] = member.value | ||
| else | ||
| str = value_inspect(obj) | ||
| # This object has no children, so we don't need to remember it in the `@var_map` | ||
| vid = 0 | ||
| end | ||
| if name | ||
Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This I moved this logic to the | ||
| { 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 | ||
Comment on lines
-1095
to
-1096
Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This separation between the code that answers "what are the members?" and "how many members are there?" made it quite difficult to make changes to the presentation objects correctly. Now both have a single source of truth, in | ||
| 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 | ||
| end | ||
| end | ||
| end | ||
Uh oh!
There was an error while loading. Please reload this page.


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. I missed the edge case for falsy value.