debug 1.3.4 → 1.6.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,68 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'irb/completion'
5
+ require 'tmpdir'
6
+ require 'fileutils'
4
7
 
5
8
  module DEBUGGER__
6
9
  module UI_DAP
7
- SHOW_PROTOCOL = ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
10
+ SHOW_PROTOCOL = ENV['DEBUG_DAP_SHOW_PROTOCOL'] == '1' || ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
11
+
12
+ def self.setup debug_port
13
+ if File.directory? '.vscode'
14
+ dir = Dir.pwd
15
+ else
16
+ dir = Dir.mktmpdir("ruby-debug-vscode-")
17
+ tempdir = true
18
+ end
19
+
20
+ at_exit do
21
+ CONFIG.skip_all
22
+ FileUtils.rm_rf dir if tempdir
23
+ end
24
+
25
+ key = rand.to_s
26
+
27
+ Dir.chdir(dir) do
28
+ Dir.mkdir('.vscode') if tempdir
29
+
30
+ # vscode-rdbg 0.0.9 or later is needed
31
+ open('.vscode/rdbg_autoattach.json', 'w') do |f|
32
+ f.puts JSON.pretty_generate({
33
+ type: "rdbg",
34
+ name: "Attach with rdbg",
35
+ request: "attach",
36
+ rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
37
+ debugPort: debug_port,
38
+ localfs: true,
39
+ autoAttach: key,
40
+ })
41
+ end
42
+ end
43
+
44
+ cmds = ['code', "#{dir}/"]
45
+ cmdline = cmds.join(' ')
46
+ ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/"
47
+
48
+ STDERR.puts "Launching: #{cmdline}"
49
+ env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
50
+ env['RUBY_DEBUG_AUTOATTACH'] = key
51
+
52
+ unless system(env, *cmds)
53
+ DEBUGGER__.warn <<~MESSAGE
54
+ Can not invoke the command.
55
+ Use the command-line on your terminal (with modification if you need).
56
+
57
+ #{cmdline}
58
+
59
+ If your application is running on a SSH remote host, please try:
60
+
61
+ #{ssh_cmdline}
62
+
63
+ MESSAGE
64
+ end
65
+ end
8
66
 
9
67
  def show_protocol dir, msg
10
68
  if SHOW_PROTOCOL
@@ -12,10 +70,70 @@ module DEBUGGER__
12
70
  end
13
71
  end
14
72
 
73
+ # true: all localfs
74
+ # Array: part of localfs
75
+ # nil: no localfs
76
+ @local_fs_map = nil
77
+
78
+ def self.remote_to_local_path path
79
+ case @local_fs_map
80
+ when nil
81
+ nil
82
+ when true
83
+ path
84
+ else # Array
85
+ @local_fs_map.each do |(remote_path_prefix, local_path_prefix)|
86
+ if path.start_with? remote_path_prefix
87
+ return path.sub(remote_path_prefix){ local_path_prefix }
88
+ end
89
+ end
90
+
91
+ nil
92
+ end
93
+ end
94
+
95
+ def self.local_to_remote_path path
96
+ case @local_fs_map
97
+ when nil
98
+ nil
99
+ when true
100
+ path
101
+ else # Array
102
+ @local_fs_map.each do |(remote_path_prefix, local_path_prefix)|
103
+ if path.start_with? local_path_prefix
104
+ return path.sub(local_path_prefix){ remote_path_prefix }
105
+ end
106
+ end
107
+
108
+ nil
109
+ end
110
+ end
111
+
112
+ def self.local_fs_map_set map
113
+ return if @local_fs_map # already setup
114
+
115
+ case map
116
+ when String
117
+ @local_fs_map = map.split(',').map{|e| e.split(':').map{|path| path.delete_suffix('/') + '/'}}
118
+ when true
119
+ @local_fs_map = map
120
+ when nil
121
+ @local_fs_map = CONFIG[:local_fs_map]
122
+ end
123
+ end
124
+
15
125
  def dap_setup bytes
16
126
  CONFIG.set_config no_color: true
17
127
  @seq = 0
18
128
 
129
+ case self
130
+ when UI_UnixDomainServer
131
+ UI_DAP.local_fs_map_set true
132
+ when UI_TcpServer
133
+ # TODO: loopback address can be used to connect other FS env, like Docker containers
134
+ # UI_DAP.local_fs_set if @local_addr.ipv4_loopback? || @local_addr.ipv6_loopback?
135
+ end
136
+
19
137
  show_protocol :>, bytes
20
138
  req = JSON.load(bytes)
21
139
 
@@ -31,24 +149,24 @@ module DEBUGGER__
31
149
  {
32
150
  filter: 'any',
33
151
  label: 'rescue any exception',
34
- #supportsCondition: true,
152
+ supportsCondition: true,
35
153
  #conditionDescription: '',
36
154
  },
37
155
  {
38
156
  filter: 'RuntimeError',
39
157
  label: 'rescue RuntimeError',
40
- default: true,
41
- #supportsCondition: true,
158
+ supportsCondition: true,
42
159
  #conditionDescription: '',
43
160
  },
44
161
  ],
45
162
  supportsExceptionFilterOptions: true,
46
163
  supportsStepBack: true,
164
+ supportsEvaluateForHovers: true,
165
+ supportsCompletionsRequest: true,
47
166
 
48
167
  ## Will be supported
49
168
  # supportsExceptionOptions: true,
50
169
  # supportsHitConditionalBreakpoints:
51
- # supportsEvaluateForHovers:
52
170
  # supportsSetVariable: true,
53
171
  # supportSuspendDebuggee:
54
172
  # supportsLogPoints:
@@ -58,7 +176,6 @@ module DEBUGGER__
58
176
 
59
177
  ## Possible?
60
178
  # supportsRestartFrame:
61
- # supportsCompletionsRequest:
62
179
  # completionTriggerCharacters:
63
180
  # supportsModulesRequest:
64
181
  # additionalModuleColumns:
@@ -84,25 +201,27 @@ module DEBUGGER__
84
201
  end
85
202
 
86
203
  def send **kw
87
- kw[:seq] = @seq += 1
88
- str = JSON.dump(kw)
89
- show_protocol '<', str
90
- @sock.write "Content-Length: #{str.size}\r\n\r\n#{str}"
204
+ if sock = @sock
205
+ kw[:seq] = @seq += 1
206
+ str = JSON.dump(kw)
207
+ sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
208
+ show_protocol '<', str
209
+ end
91
210
  end
92
211
 
93
- def send_response req, success: true, **kw
212
+ def send_response req, success: true, message: nil, **kw
94
213
  if kw.empty?
95
214
  send type: 'response',
96
215
  command: req['command'],
97
216
  request_seq: req['seq'],
98
217
  success: success,
99
- message: success ? 'Success' : 'Failed'
218
+ message: message || (success ? 'Success' : 'Failed')
100
219
  else
101
220
  send type: 'response',
102
221
  command: req['command'],
103
222
  request_seq: req['seq'],
104
223
  success: success,
105
- message: success ? 'Success' : 'Failed',
224
+ message: message || (success ? 'Success' : 'Failed'),
106
225
  body: kw
107
226
  end
108
227
  end
@@ -119,29 +238,27 @@ module DEBUGGER__
119
238
  end
120
239
 
121
240
  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
241
+ r = IO.select([@sock])
242
+
243
+ @session.process_group.sync do
244
+ raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)
245
+
246
+ case header = @sock.gets
247
+ when /Content-Length: (\d+)/
248
+ b = @sock.read(2)
249
+ raise b.inspect unless b == "\r\n"
250
+
251
+ l = @sock.read(s = $1.to_i)
252
+ show_protocol :>, l
253
+ JSON.load(l)
254
+ when nil
255
+ nil
256
+ else
257
+ raise "unrecognized line: #{l} (#{l.size} bytes)"
141
258
  end
142
- rescue RetryBecauseCantRead
143
- retry
144
259
  end
260
+ rescue RetryBecauseCantRead
261
+ retry
145
262
  end
146
263
 
147
264
  def process
@@ -154,67 +271,97 @@ module DEBUGGER__
154
271
  ## boot/configuration
155
272
  when 'launch'
156
273
  send_response req
157
- @is_attach = false
274
+ UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap')
275
+ @is_launch = true
276
+
158
277
  when 'attach'
159
278
  send_response req
160
- Process.kill(:SIGURG, Process.pid)
161
- @is_attach = true
162
- when 'setBreakpoints'
163
- path = args.dig('source', 'path')
164
- bp_args = args['breakpoints']
165
- bps = []
166
- bp_args.each{|bp|
167
- line = bp['line']
168
- if cond = bp['condition']
169
- bps << SESSION.add_line_breakpoint(path, line, cond: cond)
170
- else
171
- bps << SESSION.add_line_breakpoint(path, line)
279
+ UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap')
280
+ @is_launch = false
281
+
282
+ when 'configurationDone'
283
+ send_response req
284
+
285
+ if @is_launch
286
+ @q_msg << 'continue'
287
+ else
288
+ if SESSION.in_subsession?
289
+ send_event 'stopped', reason: 'pause',
290
+ threadId: 1, # maybe ...
291
+ allThreadsStopped: true
172
292
  end
173
- }
174
- send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
293
+ end
294
+
295
+ when 'setBreakpoints'
296
+ req_path = args.dig('source', 'path')
297
+ path = UI_DAP.local_to_remote_path(req_path)
298
+
299
+ if path
300
+ SESSION.clear_line_breakpoints path
301
+
302
+ bps = []
303
+ args['breakpoints'].each{|bp|
304
+ line = bp['line']
305
+ if cond = bp['condition']
306
+ bps << SESSION.add_line_breakpoint(path, line, cond: cond)
307
+ else
308
+ bps << SESSION.add_line_breakpoint(path, line)
309
+ end
310
+ }
311
+ send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
312
+ else
313
+ send_response req, success: false, message: "#{req_path} is not available"
314
+ end
315
+
175
316
  when 'setFunctionBreakpoints'
176
317
  send_response req
318
+
177
319
  when 'setExceptionBreakpoints'
178
- process_filter = ->(filter_id) {
179
- case filter_id
180
- when 'any'
181
- bp = SESSION.add_catch_breakpoint 'Exception'
182
- when 'RuntimeError'
183
- bp = SESSION.add_catch_breakpoint 'RuntimeError'
184
- else
185
- bp = nil
186
- end
187
- {
188
- verified: bp ? true : false,
189
- message: bp.inspect,
320
+ process_filter = ->(filter_id, cond = nil) {
321
+ bp =
322
+ case filter_id
323
+ when 'any'
324
+ SESSION.add_catch_breakpoint 'Exception', cond: cond
325
+ when 'RuntimeError'
326
+ SESSION.add_catch_breakpoint 'RuntimeError', cond: cond
327
+ else
328
+ nil
329
+ end
330
+ {
331
+ verified: !bp.nil?,
332
+ message: bp.inspect,
333
+ }
190
334
  }
191
- }
192
335
 
193
- filters = args.fetch('filters').map {|filter_id|
194
- process_filter.call(filter_id)
195
- }
336
+ SESSION.clear_catch_breakpoints 'Exception', 'RuntimeError'
196
337
 
197
- filters += args.fetch('filterOptions', {}).map{|bp_info|
198
- process_filter.call(bp_info.dig('filterId'))
338
+ filters = args.fetch('filters').map {|filter_id|
339
+ process_filter.call(filter_id)
340
+ }
341
+
342
+ filters += args.fetch('filterOptions', {}).map{|bp_info|
343
+ process_filter.call(bp_info['filterId'], bp_info['condition'])
199
344
  }
200
345
 
201
346
  send_response req, breakpoints: filters
202
- when 'configurationDone'
203
- send_response req
204
- if defined?(@is_attach) && @is_attach
205
- @q_msg << 'p'
206
- send_event 'stopped', reason: 'pause',
207
- threadId: 1,
208
- allThreadsStopped: true
209
- else
210
- @q_msg << 'continue'
211
- end
347
+
212
348
  when 'disconnect'
213
- if args.fetch("terminateDebuggee", false)
214
- @q_msg << 'kill!'
349
+ terminate = args.fetch("terminateDebuggee", false)
350
+
351
+ if SESSION.in_subsession?
352
+ if terminate
353
+ @q_msg << 'kill!'
354
+ else
355
+ @q_msg << 'continue'
356
+ end
215
357
  else
216
- @q_msg << 'continue'
358
+ if terminate
359
+ @q_msg << 'kill!'
360
+ pause
361
+ end
217
362
  end
363
+
364
+ SESSION.clear_all_breakpoints
218
365
  send_response req
219
366
 
220
367
  ## control
@@ -229,7 +376,7 @@ module DEBUGGER__
229
376
  rescue PostmortemError
230
377
  send_response req,
231
378
  success: false, message: 'postmortem mode',
232
- result: "'Next' is not supported while postrmotem mode"
379
+ result: "'Next' is not supported while postmortem mode"
233
380
  end
234
381
  when 'stepIn'
235
382
  begin
@@ -239,7 +386,7 @@ module DEBUGGER__
239
386
  rescue PostmortemError
240
387
  send_response req,
241
388
  success: false, message: 'postmortem mode',
242
- result: "'stepIn' is not supported while postrmotem mode"
389
+ result: "'stepIn' is not supported while postmortem mode"
243
390
  end
244
391
  when 'stepOut'
245
392
  begin
@@ -249,14 +396,14 @@ module DEBUGGER__
249
396
  rescue PostmortemError
250
397
  send_response req,
251
398
  success: false, message: 'postmortem mode',
252
- result: "'stepOut' is not supported while postrmotem mode"
399
+ result: "'stepOut' is not supported while postmortem mode"
253
400
  end
254
401
  when 'terminate'
255
402
  send_response req
256
403
  exit
257
404
  when 'pause'
258
405
  send_response req
259
- Process.kill(:SIGURG, Process.pid)
406
+ Process.kill(UI_ServerBase::TRAP_SIGNAL, Process.pid)
260
407
  when 'reverseContinue'
261
408
  send_response req,
262
409
  success: false, message: 'cancelled',
@@ -276,25 +423,20 @@ module DEBUGGER__
276
423
  'scopes',
277
424
  'variables',
278
425
  'evaluate',
279
- 'source'
426
+ 'source',
427
+ 'completions'
280
428
  @q_msg << req
281
429
 
282
430
  else
283
431
  raise "Unknown request: #{req.inspect}"
284
432
  end
285
433
  end
434
+ ensure
435
+ send_event :terminated unless @sock.closed?
286
436
  end
287
437
 
288
438
  ## called by the SESSION thread
289
439
 
290
- def readline prompt
291
- @q_msg.pop || 'kill!'
292
- end
293
-
294
- def sock skip: false
295
- yield $stderr
296
- end
297
-
298
440
  def respond req, res
299
441
  send_response(req, **res)
300
442
  end
@@ -306,6 +448,16 @@ module DEBUGGER__
306
448
 
307
449
  def event type, *args
308
450
  case type
451
+ when :load
452
+ file_path, reloaded = *args
453
+
454
+ if file_path
455
+ send_event 'loadedSource',
456
+ reason: (reloaded ? :changed : :new),
457
+ source: {
458
+ path: file_path,
459
+ }
460
+ end
309
461
  when :suspend_bp
310
462
  _i, bp, tid = *args
311
463
  if bp.kind_of?(CatchBreakpoint)
@@ -352,15 +504,16 @@ module DEBUGGER__
352
504
  case req['command']
353
505
  when 'stepBack'
354
506
  if @tc.recorder&.can_step_back?
355
- @tc << [:step, :back]
507
+ request_tc [:step, :back]
356
508
  else
357
509
  fail_response req, message: 'cancelled'
358
510
  end
359
511
 
360
512
  when 'stackTrace'
361
513
  tid = req.dig('arguments', 'threadId')
514
+
362
515
  if tc = find_waiting_tc(tid)
363
- tc << [:dap, :backtrace, req]
516
+ request_tc [:dap, :backtrace, req]
364
517
  else
365
518
  fail_response req
366
519
  end
@@ -369,7 +522,7 @@ module DEBUGGER__
369
522
  if @frame_map[frame_id]
370
523
  tid, fid = @frame_map[frame_id]
371
524
  if tc = find_waiting_tc(tid)
372
- tc << [:dap, :scopes, req, fid]
525
+ request_tc [:dap, :scopes, req, fid]
373
526
  else
374
527
  fail_response req
375
528
  end
@@ -382,7 +535,6 @@ module DEBUGGER__
382
535
  case ref[0]
383
536
  when :globals
384
537
  vars = global_variables.map do |name|
385
- File.write('/tmp/x', "#{name}\n")
386
538
  gv = 'Not implemented yet...'
387
539
  {
388
540
  name: name,
@@ -402,7 +554,7 @@ module DEBUGGER__
402
554
  tid, fid = @frame_map[frame_id]
403
555
 
404
556
  if tc = find_waiting_tc(tid)
405
- tc << [:dap, :scope, req, fid]
557
+ request_tc [:dap, :scope, req, fid]
406
558
  else
407
559
  fail_response req
408
560
  end
@@ -411,7 +563,7 @@ module DEBUGGER__
411
563
  tid, vid = ref[1], ref[2]
412
564
 
413
565
  if tc = find_waiting_tc(tid)
414
- tc << [:dap, :variable, req, vid]
566
+ request_tc [:dap, :variable, req, vid]
415
567
  else
416
568
  fail_response req
417
569
  end
@@ -423,11 +575,13 @@ module DEBUGGER__
423
575
  end
424
576
  when 'evaluate'
425
577
  frame_id = req.dig('arguments', 'frameId')
578
+ context = req.dig('arguments', 'context')
579
+
426
580
  if @frame_map[frame_id]
427
581
  tid, fid = @frame_map[frame_id]
428
582
  expr = req.dig('arguments', 'expression')
429
583
  if tc = find_waiting_tc(tid)
430
- tc << [:dap, :evaluate, req, fid, expr]
584
+ request_tc [:dap, :evaluate, req, fid, expr, context]
431
585
  else
432
586
  fail_response req
433
587
  end
@@ -437,11 +591,26 @@ module DEBUGGER__
437
591
  when 'source'
438
592
  ref = req.dig('arguments', 'sourceReference')
439
593
  if src = @src_map[ref]
440
- @ui.respond req, content: src.join
594
+ @ui.respond req, content: src.join("\n")
441
595
  else
442
596
  fail_response req, message: 'not found...'
443
597
  end
444
598
  return :retry
599
+
600
+ when 'completions'
601
+ frame_id = req.dig('arguments', 'frameId')
602
+ tid, fid = @frame_map[frame_id]
603
+
604
+ if tc = find_waiting_tc(tid)
605
+ text = req.dig('arguments', 'text')
606
+ line = req.dig('arguments', 'line')
607
+ if col = req.dig('arguments', 'column')
608
+ text = text.split(/\n/)[line.to_i - 1][0...(col.to_i - 1)]
609
+ end
610
+ request_tc [:dap, :completions, req, fid, text]
611
+ else
612
+ fail_response req
613
+ end
445
614
  else
446
615
  raise "Unknown DAP request: #{req.inspect}"
447
616
  end
@@ -453,13 +622,18 @@ module DEBUGGER__
453
622
 
454
623
  case type
455
624
  when :backtrace
456
- result[:stackFrames].each.with_index{|fi, i|
625
+ result[:stackFrames].each{|fi|
626
+ frame_depth = fi[:id]
457
627
  fi[:id] = id = @frame_map.size + 1
458
- @frame_map[id] = [req.dig('arguments', 'threadId'), i]
459
- if fi[:source] && src = fi[:source][:sourceReference]
460
- src_id = @src_map.size + 1
461
- @src_map[src_id] = src
462
- fi[:source][:sourceReference] = src_id
628
+ @frame_map[id] = [req.dig('arguments', 'threadId'), frame_depth]
629
+ if fi[:source]
630
+ if src = fi[:source][:sourceReference]
631
+ src_id = @src_map.size + 1
632
+ @src_map[src_id] = src
633
+ fi[:source][:sourceReference] = src_id
634
+ else
635
+ fi[:source][:sourceReference] = 0
636
+ end
463
637
  end
464
638
  }
465
639
  @ui.respond req, result
@@ -479,8 +653,15 @@ module DEBUGGER__
479
653
  register_vars result[:variables], tid
480
654
  @ui.respond req, result
481
655
  when :evaluate
482
- tid = result.delete :tid
483
- register_var result, tid
656
+ message = result.delete :message
657
+ if message
658
+ @ui.respond req, success: false, message: message
659
+ else
660
+ tid = result.delete :tid
661
+ register_var result, tid
662
+ @ui.respond req, result
663
+ end
664
+ when :completions
484
665
  @ui.respond req, result
485
666
  else
486
667
  raise "unsupported: #{args.inspect}"
@@ -504,6 +685,11 @@ module DEBUGGER__
504
685
  end
505
686
 
506
687
  class ThreadClient
688
+ def value_inspect obj
689
+ # TODO: max length should be configuarable?
690
+ DEBUGGER__.safe_inspect obj, short: true, max_length: 4 * 1024
691
+ end
692
+
507
693
  def process_dap args
508
694
  # pp tc: self, args: args
509
695
  type = args.shift
@@ -511,27 +697,42 @@ module DEBUGGER__
511
697
 
512
698
  case type
513
699
  when :backtrace
514
- event! :dap_result, :backtrace, req, {
515
- stackFrames: @target_frames.map{|frame|
516
- path = frame.realpath || frame.path
517
- ref = frame.file_lines unless path && File.exist?(path)
518
-
519
- {
520
- # id: ??? # filled by SESSION
521
- name: frame.name,
522
- line: frame.location.lineno,
523
- column: 1,
524
- source: {
525
- name: File.basename(frame.path),
526
- path: path,
527
- sourceReference: ref,
528
- },
529
- }
700
+ start_frame = req.dig('arguments', 'startFrame') || 0
701
+ levels = req.dig('arguments', 'levels') || 1_000
702
+ frames = []
703
+ @target_frames.each_with_index do |frame, i|
704
+ next if i < start_frame
705
+ break if (levels -= 1) < 0
706
+
707
+ path = frame.realpath || frame.path
708
+ source_name = path ? File.basename(path) : frame.location.to_s
709
+
710
+ if (path && File.exist?(path)) && (local_path = UI_DAP.remote_to_local_path(path))
711
+ # ok
712
+ else
713
+ ref = frame.file_lines
714
+ end
715
+
716
+ frames << {
717
+ id: i, # id is refilled by SESSION
718
+ name: frame.name,
719
+ line: frame.location.lineno,
720
+ column: 1,
721
+ source: {
722
+ name: source_name,
723
+ path: (local_path || path),
724
+ sourceReference: ref,
725
+ },
530
726
  }
727
+ end
728
+
729
+ event! :dap_result, :backtrace, req, {
730
+ stackFrames: frames,
731
+ totalFrames: @target_frames.size,
531
732
  }
532
733
  when :scopes
533
734
  fid = args.shift
534
- frame = @target_frames[fid]
735
+ frame = get_frame(fid)
535
736
 
536
737
  lnum =
537
738
  if frame.binding
@@ -559,26 +760,12 @@ module DEBUGGER__
559
760
  }]
560
761
  when :scope
561
762
  fid = args.shift
562
- frame = @target_frames[fid]
563
- if b = frame.binding
564
- vars = b.local_variables.map{|name|
565
- v = b.local_variable_get(name)
566
- variable(name, v)
567
- }
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
570
- vars.unshift variable('%self', b.receiver)
571
- elsif lvars = frame.local_variables
572
- vars = lvars.map{|var, val|
573
- variable(var, val)
574
- }
575
- else
576
- 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
763
+ frame = get_frame(fid)
764
+ vars = collect_locals(frame).map do |var, val|
765
+ variable(var, val)
579
766
  end
580
- event! :dap_result, :scope, req, variables: vars, tid: self.id
581
767
 
768
+ event! :dap_result, :scope, req, variables: vars, tid: self.id
582
769
  when :variable
583
770
  vid = args.shift
584
771
  obj = @var_map[vid]
@@ -596,7 +783,7 @@ module DEBUGGER__
596
783
  case obj
597
784
  when Hash
598
785
  vars = obj.map{|k, v|
599
- variable(DEBUGGER__.short_inspect(k), v)
786
+ variable(value_inspect(k), v,)
600
787
  }
601
788
  when Struct
602
789
  vars = obj.members.map{|m|
@@ -619,41 +806,135 @@ module DEBUGGER__
619
806
  ]
620
807
  end
621
808
 
622
- vars += obj.instance_variables.map{|iv|
623
- variable(iv, obj.instance_variable_get(iv))
809
+ vars += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
810
+ variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
624
811
  }
625
- vars.unshift variable('#class', obj.class)
812
+ vars.unshift variable('#class', M_CLASS.bind_call(obj))
626
813
  end
627
814
  end
628
815
  event! :dap_result, :variable, req, variables: (vars || []), tid: self.id
629
816
 
630
817
  when :evaluate
631
- fid, expr = args
632
- frame = @target_frames[fid]
818
+ fid, expr, context = args
819
+ frame = get_frame(fid)
820
+ message = nil
633
821
 
634
- if frame && (b = frame.binding)
635
- begin
636
- result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
637
- rescue Exception => e
638
- result = e
822
+ if frame && (b = frame.eval_binding)
823
+ special_local_variables frame do |name, var|
824
+ b.local_variable_set(name, var) if /\%/ !~ name
825
+ end
826
+
827
+ case context
828
+ when 'repl', 'watch'
829
+ begin
830
+ result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
831
+ rescue Exception => e
832
+ result = e
833
+ end
834
+
835
+ when 'hover'
836
+ case expr
837
+ when /\A\@\S/
838
+ begin
839
+ result = M_INSTANCE_VARIABLE_GET.bind_call(b.receiver, expr)
840
+ rescue NameError
841
+ message = "Error: Not defined instance variable: #{expr.inspect}"
842
+ end
843
+ when /\A\$\S/
844
+ global_variables.each{|gvar|
845
+ if gvar.to_s == expr
846
+ result = eval(gvar.to_s)
847
+ break false
848
+ end
849
+ } and (message = "Error: Not defined global variable: #{expr.inspect}")
850
+ when /\Aself$/
851
+ result = b.receiver
852
+ when /(\A((::[A-Z]|[A-Z])\w*)+)/
853
+ unless result = search_const(b, $1)
854
+ message = "Error: Not defined constants: #{expr.inspect}"
855
+ end
856
+ else
857
+ begin
858
+ result = b.local_variable_get(expr)
859
+ rescue NameError
860
+ # try to check method
861
+ if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true)
862
+ result = M_METHOD.bind_call(b.receiver, expr)
863
+ else
864
+ message = "Error: Can not evaluate: #{expr.inspect}"
865
+ end
866
+ end
867
+ end
868
+ else
869
+ message = "Error: unknown context: #{context}"
639
870
  end
640
871
  else
641
- result = 'can not evaluate on this frame...'
872
+ result = 'Error: Can not evaluate on this frame'
642
873
  end
643
- event! :dap_result, :evaluate, req, tid: self.id, **evaluate_result(result)
874
+
875
+ event! :dap_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result)
876
+
877
+ when :completions
878
+ fid, text = args
879
+ frame = get_frame(fid)
880
+
881
+ if (b = frame&.binding) && word = text&.split(/[\s\{]/)&.last
882
+ words = IRB::InputCompletor::retrieve_completion_data(word, bind: b).compact
883
+ end
884
+
885
+ event! :dap_result, :completions, req, targets: (words || []).map{|phrase|
886
+ detail = nil
887
+
888
+ if /\b([_a-zA-Z]\w*[!\?]?)\z/ =~ phrase
889
+ w = $1
890
+ else
891
+ w = phrase
892
+ end
893
+
894
+ begin
895
+ v = b.local_variable_get(w)
896
+ detail ="(variable: #{value_inspect(v)})"
897
+ rescue NameError
898
+ end
899
+
900
+ {
901
+ label: phrase,
902
+ text: w,
903
+ detail: detail,
904
+ }
905
+ }
906
+
644
907
  else
645
908
  raise "Unknown req: #{args.inspect}"
646
909
  end
647
910
  end
648
911
 
912
+ def search_const b, expr
913
+ cs = expr.delete_prefix('::').split('::')
914
+ [Object, *b.eval('Module.nesting')].reverse_each{|mod|
915
+ if cs.all?{|c|
916
+ if mod.const_defined?(c)
917
+ mod = mod.const_get(c)
918
+ else
919
+ false
920
+ end
921
+ }
922
+ # if-body
923
+ return mod
924
+ end
925
+ }
926
+ false
927
+ end
928
+
649
929
  def evaluate_result r
650
930
  v = variable nil, r
651
- v.delete(:name)
652
- v[:result] = DEBUGGER__.short_inspect(r)
931
+ v.delete :name
932
+ v.delete :value
933
+ v[:result] = value_inspect(r)
653
934
  v
654
935
  end
655
936
 
656
- def variable_ name, obj, indexedVariables: 0, namedVariables: 0, use_short: true
937
+ def variable_ name, obj, indexedVariables: 0, namedVariables: 0
657
938
  if indexedVariables > 0 || namedVariables > 0
658
939
  vid = @var_map.size + 1
659
940
  @var_map[vid] = obj
@@ -661,11 +942,11 @@ module DEBUGGER__
661
942
  vid = 0
662
943
  end
663
944
 
664
- ivnum = obj.instance_variables.size
945
+ ivnum = M_INSTANCE_VARIABLES.bind_call(obj).size
665
946
 
666
947
  { name: name,
667
- value: DEBUGGER__.short_inspect(obj, use_short),
668
- type: obj.class.name || obj.class.to_s,
948
+ value: value_inspect(obj),
949
+ type: (klass = M_CLASS.bind_call(obj)).name || klass.to_s,
669
950
  variablesReference: vid,
670
951
  indexedVariables: indexedVariables,
671
952
  namedVariables: namedVariables + ivnum,
@@ -679,7 +960,7 @@ module DEBUGGER__
679
960
  when Hash
680
961
  variable_ name, obj, namedVariables: obj.size
681
962
  when String
682
- variable_ name, obj, use_short: false, namedVariables: 3 # #to_str, #length, #encoding
963
+ variable_ name, obj, namedVariables: 3 # #to_str, #length, #encoding
683
964
  when Struct
684
965
  variable_ name, obj, namedVariables: obj.size
685
966
  when Class, Module