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.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +234 -9
- data/Gemfile +1 -0
- data/README.md +81 -31
- data/Rakefile +28 -10
- data/debug.gemspec +7 -5
- data/exe/rdbg +7 -3
- data/ext/debug/debug.c +80 -15
- data/ext/debug/extconf.rb +22 -0
- data/lib/debug/breakpoint.rb +141 -67
- data/lib/debug/client.rb +77 -20
- data/lib/debug/color.rb +29 -19
- data/lib/debug/config.rb +61 -27
- data/lib/debug/console.rb +59 -18
- data/lib/debug/frame_info.rb +41 -40
- data/lib/debug/local.rb +1 -1
- data/lib/debug/prelude.rb +2 -2
- data/lib/debug/server.rb +136 -103
- data/lib/debug/server_cdp.rb +880 -162
- data/lib/debug/server_dap.rb +445 -164
- data/lib/debug/session.rb +540 -269
- data/lib/debug/source_repository.rb +103 -52
- data/lib/debug/thread_client.rb +306 -138
- data/lib/debug/tracer.rb +8 -13
- data/lib/debug/version.rb +1 -1
- data/misc/README.md.erb +44 -16
- metadata +6 -15
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
- data/.github/ISSUE_TEMPLATE/custom.md +0 -10
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
- data/.github/workflows/ruby.yml +0 -34
- data/.gitignore +0 -12
- data/bin/console +0 -14
- data/bin/gentest +0 -22
- data/bin/setup +0 -8
- data/lib/debug/bp.vim +0 -68
data/lib/debug/server_dap.rb
CHANGED
@@ -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
|
-
|
152
|
+
supportsCondition: true,
|
35
153
|
#conditionDescription: '',
|
36
154
|
},
|
37
155
|
{
|
38
156
|
filter: 'RuntimeError',
|
39
157
|
label: 'rescue RuntimeError',
|
40
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
@
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
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
|
-
|
161
|
-
@
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
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
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
-
|
194
|
-
process_filter.call(filter_id)
|
195
|
-
}
|
336
|
+
SESSION.clear_catch_breakpoints 'Exception', 'RuntimeError'
|
196
337
|
|
197
|
-
|
198
|
-
|
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
|
-
|
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
|
-
|
214
|
-
|
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
|
-
|
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
|
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
|
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
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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'),
|
459
|
-
if fi[:source]
|
460
|
-
|
461
|
-
|
462
|
-
|
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
|
-
|
483
|
-
|
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
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
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 =
|
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 =
|
563
|
-
|
564
|
-
|
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(
|
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.
|
623
|
-
variable(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
|
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 =
|
818
|
+
fid, expr, context = args
|
819
|
+
frame = get_frame(fid)
|
820
|
+
message = nil
|
633
821
|
|
634
|
-
if frame && (b = frame.
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
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 = '
|
872
|
+
result = 'Error: Can not evaluate on this frame'
|
642
873
|
end
|
643
|
-
|
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
|
652
|
-
v
|
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
|
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.
|
945
|
+
ivnum = M_INSTANCE_VARIABLES.bind_call(obj).size
|
665
946
|
|
666
947
|
{ name: name,
|
667
|
-
value:
|
668
|
-
type: obj.
|
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,
|
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
|