debug 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'irb/completion'
5
+ require 'tmpdir'
6
+ require 'json'
7
+ require 'fileutils'
4
8
 
5
9
  module DEBUGGER__
6
10
  module UI_DAP
7
- SHOW_PROTOCOL = ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
11
+ SHOW_PROTOCOL = ENV['DEBUG_DAP_SHOW_PROTOCOL'] == '1' || ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
12
+
13
+ def self.setup sock_path
14
+ dir = Dir.mktmpdir("ruby-debug-vscode-")
15
+ at_exit{
16
+ CONFIG[:skip_path] = [//] # skip all
17
+ FileUtils.rm_rf dir
18
+ }
19
+ Dir.chdir(dir) do
20
+ Dir.mkdir('.vscode')
21
+ open('README.rb', 'w'){|f|
22
+ f.puts <<~MSG
23
+ # Wait for starting the attaching to the Ruby process
24
+ # This file will be removed at the end of the debuggee process.
25
+ #
26
+ # Note that vscode-rdbg extension is needed. Please install if you don't have.
27
+ MSG
28
+ }
29
+ open('.vscode/launch.json', 'w'){|f|
30
+ f.puts JSON.pretty_generate({
31
+ version: '0.2.0',
32
+ configurations: [
33
+ {
34
+ type: "rdbg",
35
+ name: "Attach with rdbg",
36
+ request: "attach",
37
+ rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
38
+ debugPort: sock_path,
39
+ autoAttach: true,
40
+ }
41
+ ]
42
+ })
43
+ }
44
+ end
45
+
46
+ cmds = ['code', "#{dir}/", "#{dir}/README.rb"]
47
+ cmdline = cmds.join(' ')
48
+ ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/ #{dir}/README.rb"
49
+
50
+ STDERR.puts "Launching: #{cmdline}"
51
+ env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
52
+
53
+ unless system(env, *cmds)
54
+ DEBUGGER__.warn <<~MESSAGE
55
+ Can not invoke the command.
56
+ Use the command-line on your terminal (with modification if you need).
57
+
58
+ #{cmdline}
59
+
60
+ If your application is running on a SSH remote host, please try:
61
+
62
+ #{ssh_cmdline}
63
+
64
+ MESSAGE
65
+ end
66
+ end
8
67
 
9
68
  def show_protocol dir, msg
10
69
  if SHOW_PROTOCOL
@@ -44,11 +103,12 @@ module DEBUGGER__
44
103
  ],
45
104
  supportsExceptionFilterOptions: true,
46
105
  supportsStepBack: true,
106
+ supportsEvaluateForHovers: true,
107
+ supportsCompletionsRequest: true,
47
108
 
48
109
  ## Will be supported
49
110
  # supportsExceptionOptions: true,
50
111
  # supportsHitConditionalBreakpoints:
51
- # supportsEvaluateForHovers:
52
112
  # supportsSetVariable: true,
53
113
  # supportSuspendDebuggee:
54
114
  # supportsLogPoints:
@@ -58,7 +118,6 @@ module DEBUGGER__
58
118
 
59
119
  ## Possible?
60
120
  # supportsRestartFrame:
61
- # supportsCompletionsRequest:
62
121
  # completionTriggerCharacters:
63
122
  # supportsModulesRequest:
64
123
  # additionalModuleColumns:
@@ -87,22 +146,22 @@ module DEBUGGER__
87
146
  kw[:seq] = @seq += 1
88
147
  str = JSON.dump(kw)
89
148
  show_protocol '<', str
90
- @sock.write "Content-Length: #{str.size}\r\n\r\n#{str}"
149
+ @sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
91
150
  end
92
151
 
93
- def send_response req, success: true, **kw
152
+ def send_response req, success: true, message: nil, **kw
94
153
  if kw.empty?
95
154
  send type: 'response',
96
155
  command: req['command'],
97
156
  request_seq: req['seq'],
98
157
  success: success,
99
- message: success ? 'Success' : 'Failed'
158
+ message: message || (success ? 'Success' : 'Failed')
100
159
  else
101
160
  send type: 'response',
102
161
  command: req['command'],
103
162
  request_seq: req['seq'],
104
163
  success: success,
105
- message: success ? 'Success' : 'Failed',
164
+ message: message || (success ? 'Success' : 'Failed'),
106
165
  body: kw
107
166
  end
108
167
  end
@@ -119,29 +178,27 @@ module DEBUGGER__
119
178
  end
120
179
 
121
180
  def recv_request
122
- begin
123
- r = IO.select([@sock])
124
-
125
- @session.process_group.sync do
126
- raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)
127
-
128
- case header = @sock.gets
129
- when /Content-Length: (\d+)/
130
- b = @sock.read(2)
131
- raise b.inspect unless b == "\r\n"
132
-
133
- l = @sock.read(s = $1.to_i)
134
- show_protocol :>, l
135
- JSON.load(l)
136
- when nil
137
- nil
138
- else
139
- raise "unrecognized line: #{l} (#{l.size} bytes)"
140
- end
181
+ r = IO.select([@sock])
182
+
183
+ @session.process_group.sync do
184
+ raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)
185
+
186
+ case header = @sock.gets
187
+ when /Content-Length: (\d+)/
188
+ b = @sock.read(2)
189
+ raise b.inspect unless b == "\r\n"
190
+
191
+ l = @sock.read(s = $1.to_i)
192
+ show_protocol :>, l
193
+ JSON.load(l)
194
+ when nil
195
+ nil
196
+ else
197
+ raise "unrecognized line: #{l} (#{l.size} bytes)"
141
198
  end
142
- rescue RetryBecauseCantRead
143
- retry
144
199
  end
200
+ rescue RetryBecauseCantRead
201
+ retry
145
202
  end
146
203
 
147
204
  def process
@@ -229,7 +286,7 @@ module DEBUGGER__
229
286
  rescue PostmortemError
230
287
  send_response req,
231
288
  success: false, message: 'postmortem mode',
232
- result: "'Next' is not supported while postrmotem mode"
289
+ result: "'Next' is not supported while postmortem mode"
233
290
  end
234
291
  when 'stepIn'
235
292
  begin
@@ -239,7 +296,7 @@ module DEBUGGER__
239
296
  rescue PostmortemError
240
297
  send_response req,
241
298
  success: false, message: 'postmortem mode',
242
- result: "'stepIn' is not supported while postrmotem mode"
299
+ result: "'stepIn' is not supported while postmortem mode"
243
300
  end
244
301
  when 'stepOut'
245
302
  begin
@@ -249,7 +306,7 @@ module DEBUGGER__
249
306
  rescue PostmortemError
250
307
  send_response req,
251
308
  success: false, message: 'postmortem mode',
252
- result: "'stepOut' is not supported while postrmotem mode"
309
+ result: "'stepOut' is not supported while postmortem mode"
253
310
  end
254
311
  when 'terminate'
255
312
  send_response req
@@ -276,7 +333,8 @@ module DEBUGGER__
276
333
  'scopes',
277
334
  'variables',
278
335
  'evaluate',
279
- 'source'
336
+ 'source',
337
+ 'completions'
280
338
  @q_msg << req
281
339
 
282
340
  else
@@ -382,7 +440,6 @@ module DEBUGGER__
382
440
  case ref[0]
383
441
  when :globals
384
442
  vars = global_variables.map do |name|
385
- File.write('/tmp/x', "#{name}\n")
386
443
  gv = 'Not implemented yet...'
387
444
  {
388
445
  name: name,
@@ -423,11 +480,13 @@ module DEBUGGER__
423
480
  end
424
481
  when 'evaluate'
425
482
  frame_id = req.dig('arguments', 'frameId')
483
+ context = req.dig('arguments', 'context')
484
+
426
485
  if @frame_map[frame_id]
427
486
  tid, fid = @frame_map[frame_id]
428
487
  expr = req.dig('arguments', 'expression')
429
488
  if tc = find_waiting_tc(tid)
430
- tc << [:dap, :evaluate, req, fid, expr]
489
+ tc << [:dap, :evaluate, req, fid, expr, context]
431
490
  else
432
491
  fail_response req
433
492
  end
@@ -442,6 +501,21 @@ module DEBUGGER__
442
501
  fail_response req, message: 'not found...'
443
502
  end
444
503
  return :retry
504
+
505
+ when 'completions'
506
+ frame_id = req.dig('arguments', 'frameId')
507
+ tid, fid = @frame_map[frame_id]
508
+
509
+ if tc = find_waiting_tc(tid)
510
+ text = req.dig('arguments', 'text')
511
+ line = req.dig('arguments', 'line')
512
+ if col = req.dig('arguments', 'column')
513
+ text = text.split(/\n/)[line.to_i - 1][0...(col.to_i - 1)]
514
+ end
515
+ tc << [:dap, :completions, req, fid, text]
516
+ else
517
+ fail_response req
518
+ end
445
519
  else
446
520
  raise "Unknown DAP request: #{req.inspect}"
447
521
  end
@@ -479,8 +553,15 @@ module DEBUGGER__
479
553
  register_vars result[:variables], tid
480
554
  @ui.respond req, result
481
555
  when :evaluate
482
- tid = result.delete :tid
483
- register_var result, tid
556
+ message = result.delete :message
557
+ if message
558
+ @ui.respond req, success: false, message: message
559
+ else
560
+ tid = result.delete :tid
561
+ register_var result, tid
562
+ @ui.respond req, result
563
+ end
564
+ when :completions
484
565
  @ui.respond req, result
485
566
  else
486
567
  raise "unsupported: #{args.inspect}"
@@ -565,8 +646,9 @@ module DEBUGGER__
565
646
  v = b.local_variable_get(name)
566
647
  variable(name, v)
567
648
  }
568
- vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
569
- vars.unshift variable('%return', frame.return_value) if frame.has_return_value
649
+ special_local_variables frame do |name, val|
650
+ vars.unshift variable(name, val)
651
+ end
570
652
  vars.unshift variable('%self', b.receiver)
571
653
  elsif lvars = frame.local_variables
572
654
  vars = lvars.map{|var, val|
@@ -574,8 +656,9 @@ module DEBUGGER__
574
656
  }
575
657
  else
576
658
  vars = [variable('%self', frame.self)]
577
- vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
578
- vars.push variable('%return', frame.return_value) if frame.has_return_value
659
+ special_local_variables frame do |name, val|
660
+ vars.push variable(name, val)
661
+ end
579
662
  end
580
663
  event! :dap_result, :scope, req, variables: vars, tid: self.id
581
664
 
@@ -596,7 +679,7 @@ module DEBUGGER__
596
679
  case obj
597
680
  when Hash
598
681
  vars = obj.map{|k, v|
599
- variable(DEBUGGER__.short_inspect(k), v)
682
+ variable(DEBUGGER__.safe_inspect(k), v,)
600
683
  }
601
684
  when Struct
602
685
  vars = obj.members.map{|m|
@@ -628,32 +711,127 @@ module DEBUGGER__
628
711
  event! :dap_result, :variable, req, variables: (vars || []), tid: self.id
629
712
 
630
713
  when :evaluate
631
- fid, expr = args
714
+ fid, expr, context = args
632
715
  frame = @target_frames[fid]
716
+ message = nil
633
717
 
634
718
  if frame && (b = frame.binding)
635
- begin
636
- result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
637
- rescue Exception => e
638
- result = e
719
+ b = b.dup
720
+ special_local_variables current_frame do |name, var|
721
+ b.local_variable_set(name, var) if /\%/ !~ name
722
+ end
723
+
724
+ case context
725
+ when 'repl', 'watch'
726
+ begin
727
+ result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
728
+ rescue Exception => e
729
+ result = e
730
+ end
731
+
732
+ when 'hover'
733
+ case expr
734
+ when /\A\@\S/
735
+ begin
736
+ (r = b.receiver).instance_variable_defined?(expr) or raise(NameError)
737
+ result = r.instance_variable_get(expr)
738
+ rescue NameError
739
+ message = "Error: Not defined instance variable: #{expr.inspect}"
740
+ end
741
+ when /\A\$\S/
742
+ global_variables.each{|gvar|
743
+ if gvar.to_s == expr
744
+ result = eval(gvar.to_s)
745
+ break false
746
+ end
747
+ } and (message = "Error: Not defined global variable: #{expr.inspect}")
748
+ when /\A[A-Z]/
749
+ unless result = search_const(b, expr)
750
+ message = "Error: Not defined constants: #{expr.inspect}"
751
+ end
752
+ else
753
+ begin
754
+ # try to check local variables
755
+ b.local_variable_defined?(expr) or raise NameError
756
+ result = b.local_variable_get(expr)
757
+ rescue NameError
758
+ # try to check method
759
+ if b.receiver.respond_to? expr, include_all: true
760
+ result = b.receiver.method(expr)
761
+ else
762
+ message = "Error: Can not evaluate: #{expr.inspect}"
763
+ end
764
+ end
765
+ end
766
+ else
767
+ message = "Error: unknown context: #{context}"
639
768
  end
640
769
  else
641
- result = 'can not evaluate on this frame...'
770
+ result = 'Error: Can not evaluate on this frame'
771
+ end
772
+
773
+ event! :dap_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result)
774
+
775
+ when :completions
776
+ fid, text = args
777
+ frame = @target_frames[fid]
778
+
779
+ if (b = frame&.binding) && word = text&.split(/[\s\{]/)&.last
780
+ words = IRB::InputCompletor::retrieve_completion_data(word, bind: b).compact
642
781
  end
643
- event! :dap_result, :evaluate, req, tid: self.id, **evaluate_result(result)
782
+
783
+ event! :dap_result, :completions, req, targets: (words || []).map{|phrase|
784
+ if /\b([_a-zA-Z]\w*[!\?]?)\z/ =~ phrase
785
+ w = $1
786
+ else
787
+ w = phrase
788
+ end
789
+
790
+ begin
791
+ if b&.local_variable_defined?(w)
792
+ v = b.local_variable_get(w)
793
+ phrase += " (variable:#{DEBUGGER__.safe_inspect(v)})"
794
+ end
795
+ rescue NameError
796
+ end
797
+
798
+ {
799
+ label: phrase,
800
+ text: w,
801
+ }
802
+ }
803
+
644
804
  else
645
805
  raise "Unknown req: #{args.inspect}"
646
806
  end
647
807
  end
648
808
 
809
+ def search_const b, expr
810
+ cs = expr.split('::')
811
+ [Object, *b.eval('Module.nesting')].reverse_each{|mod|
812
+ if cs.all?{|c|
813
+ if mod.const_defined?(c)
814
+ mod = mod.const_get(c)
815
+ else
816
+ false
817
+ end
818
+ }
819
+ # if-body
820
+ return mod
821
+ end
822
+ }
823
+ false
824
+ end
825
+
649
826
  def evaluate_result r
650
827
  v = variable nil, r
651
- v.delete(:name)
652
- v[:result] = DEBUGGER__.short_inspect(r)
828
+ v.delete :name
829
+ v.delete :value
830
+ v[:result] = DEBUGGER__.safe_inspect(r)
653
831
  v
654
832
  end
655
833
 
656
- def variable_ name, obj, indexedVariables: 0, namedVariables: 0, use_short: true
834
+ def variable_ name, obj, indexedVariables: 0, namedVariables: 0
657
835
  if indexedVariables > 0 || namedVariables > 0
658
836
  vid = @var_map.size + 1
659
837
  @var_map[vid] = obj
@@ -664,7 +842,7 @@ module DEBUGGER__
664
842
  ivnum = obj.instance_variables.size
665
843
 
666
844
  { name: name,
667
- value: DEBUGGER__.short_inspect(obj, use_short),
845
+ value: DEBUGGER__.safe_inspect(obj),
668
846
  type: obj.class.name || obj.class.to_s,
669
847
  variablesReference: vid,
670
848
  indexedVariables: indexedVariables,
@@ -679,7 +857,7 @@ module DEBUGGER__
679
857
  when Hash
680
858
  variable_ name, obj, namedVariables: obj.size
681
859
  when String
682
- variable_ name, obj, use_short: false, namedVariables: 3 # #to_str, #length, #encoding
860
+ variable_ name, obj, namedVariables: 3 # #to_str, #length, #encoding
683
861
  when Struct
684
862
  variable_ name, obj, namedVariables: obj.size
685
863
  when Class, Module