nrepl-lazuli 0.5.0 → 0.7.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/lib/nrepl-lazuli/bencode.rb +96 -0
- data/lib/nrepl-lazuli/client.rb +66 -0
- data/lib/nrepl-lazuli/connection.rb +174 -13
- data/lib/nrepl-lazuli/server.rb +7 -12
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 850adfc6733e86d502ffd88241f4dff665e34870c6c2a7d6a436ee77685565e8
|
|
4
|
+
data.tar.gz: 50d6783deae1558218fc67a1d47590c2e92d07f964daa7038d3b6f51aec2d7dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6133a4d27d93dcaf60bb06c4415384cb09e31bf08a89dd03059181fec2eba3ebc7695a890a77f8349fdea91eda292d2e8444ae365e8d59a00f96b41dec435bd8
|
|
7
|
+
data.tar.gz: a3df651980e3684fd0fffd2ef2a400dfdcf25b79ffc3e554c8425e9a880d929effc0b9fb62631f5ea8a6cb84c6ec72e80b07b7c7b3ea850372fc8f85b0239d12
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
class BEncode
|
|
2
|
+
def self.encode(data)
|
|
3
|
+
case data
|
|
4
|
+
when Integer
|
|
5
|
+
"i#{data}e"
|
|
6
|
+
when String
|
|
7
|
+
"#{data.bytesize}:#{data}"
|
|
8
|
+
when Array
|
|
9
|
+
"l#{data.map { |e| encode(e) }.join}e"
|
|
10
|
+
when Hash
|
|
11
|
+
"d#{data.map { |k, v| encode(k) + encode(v) }.join}e"
|
|
12
|
+
else
|
|
13
|
+
raise ArgumentError, "Cannot BEncode type: #{data.class} on text #{data.inspect}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(stream)
|
|
18
|
+
@stream =
|
|
19
|
+
if stream.kind_of?(IO) || stream.kind_of?(StringIO)
|
|
20
|
+
stream
|
|
21
|
+
elsif stream.respond_to? :string
|
|
22
|
+
StringIO.new stream.string
|
|
23
|
+
elsif stream.respond_to? :to_s
|
|
24
|
+
StringIO.new stream.to_s
|
|
25
|
+
end
|
|
26
|
+
@encoding = @stream.external_encoding || Encoding::default_external
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def parse!
|
|
30
|
+
case peek
|
|
31
|
+
when ?i then parse_integer!
|
|
32
|
+
when ?l then parse_list!
|
|
33
|
+
when ?d then parse_dict!
|
|
34
|
+
when ?0 .. ?9 then parse_string!
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def eos?
|
|
39
|
+
@stream.eof?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def parse_integer!
|
|
45
|
+
@stream.getc
|
|
46
|
+
num = @stream.gets("e") or raise ArgumentError
|
|
47
|
+
num.chop.to_i
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def parse_list!
|
|
51
|
+
@stream.getc
|
|
52
|
+
ary = []
|
|
53
|
+
ary.push(parse!) until peek == ?e
|
|
54
|
+
@stream.getc
|
|
55
|
+
ary
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parse_dict!
|
|
59
|
+
@stream.getc
|
|
60
|
+
hsh = {}
|
|
61
|
+
until peek == ?e
|
|
62
|
+
key = parse!
|
|
63
|
+
|
|
64
|
+
unless key.is_a? String or key.is_a? Integer
|
|
65
|
+
raise ArgumentError, "key must be a string or number"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
val = parse!
|
|
69
|
+
|
|
70
|
+
hsh.store(key.to_s, val)
|
|
71
|
+
end
|
|
72
|
+
@stream.getc
|
|
73
|
+
hsh
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_string!
|
|
77
|
+
num = @stream.gets(":") or
|
|
78
|
+
raise ArgumentError, "invalid string length (no colon)"
|
|
79
|
+
|
|
80
|
+
begin
|
|
81
|
+
length = num.chop.to_i
|
|
82
|
+
return "" if length == 0 # Workaround for Rubinius bug
|
|
83
|
+
str = @stream.read(length).force_encoding(@encoding)
|
|
84
|
+
rescue => e
|
|
85
|
+
raise ArgumentError, "invalid string length #{e}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
str
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def peek
|
|
92
|
+
c = @stream.getc
|
|
93
|
+
@stream.ungetc(c)
|
|
94
|
+
c
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'bencode'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
module NREPL
|
|
7
|
+
class Client
|
|
8
|
+
attr_reader :host, :port
|
|
9
|
+
|
|
10
|
+
def initialize(port: DEFAULT_PORT, host: DEFAULT_HOST)
|
|
11
|
+
@port = port
|
|
12
|
+
@host = host
|
|
13
|
+
@socket = TCPSocket.new(@host, @port)
|
|
14
|
+
@bencode = BEncode.new(@socket)
|
|
15
|
+
@last_id = 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def stop
|
|
19
|
+
unless closed?
|
|
20
|
+
@socket.close
|
|
21
|
+
@socket = nil
|
|
22
|
+
@bencode = nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def rpc(msg)
|
|
27
|
+
id = msg["id"] || msg[:id]
|
|
28
|
+
if(!id)
|
|
29
|
+
@last_id += 1
|
|
30
|
+
id = "autogen_#@last_id"
|
|
31
|
+
msg = msg.merge('id' => id)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
write(msg)
|
|
35
|
+
Timeout.timeout(10) do
|
|
36
|
+
loop do
|
|
37
|
+
ret = read
|
|
38
|
+
if(ret["id"] == id)
|
|
39
|
+
break ret
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def write(msg)
|
|
46
|
+
@socket.write(BEncode.encode(msg))
|
|
47
|
+
@socket.flush
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def read
|
|
51
|
+
@bencode.parse!
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def register_session
|
|
55
|
+
write('op' => 'clone')
|
|
56
|
+
msg = read!
|
|
57
|
+
|
|
58
|
+
raise ArgumentError, "failed to create session" unless msg['new_session']
|
|
59
|
+
msg['new_session']
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def closed?
|
|
63
|
+
@socket.nil?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -43,6 +43,10 @@ module NREPL
|
|
|
43
43
|
case msg['op']
|
|
44
44
|
when 'clone'
|
|
45
45
|
register_session(msg)
|
|
46
|
+
when 'complete'
|
|
47
|
+
get_completions(msg)
|
|
48
|
+
when 'find_definition'
|
|
49
|
+
find_definition(msg)
|
|
46
50
|
when 'describe'
|
|
47
51
|
describe_msg(msg)
|
|
48
52
|
when 'eval'
|
|
@@ -152,6 +156,97 @@ module NREPL
|
|
|
152
156
|
end
|
|
153
157
|
end
|
|
154
158
|
|
|
159
|
+
private def get_completions(msg)
|
|
160
|
+
wrap_all = ->(coll, type) do
|
|
161
|
+
coll.lazy.map { |c| { 'candidate' => c.to_s, 'type' => type } }
|
|
162
|
+
end
|
|
163
|
+
prefix_reg = Regexp.new(msg['prefix'].split('').map { |x| Regexp.escape(x) }.join('.*'))
|
|
164
|
+
context = msg.fetch('context', {})
|
|
165
|
+
methods = ->(callee) do
|
|
166
|
+
wrap_all.(callee.public_methods, 'method/public') +
|
|
167
|
+
wrap_all.(callee.protected_methods, 'method/protected') +
|
|
168
|
+
wrap_all.(callee.private_methods, 'method/private')
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
completions = case context['type']
|
|
172
|
+
when 'symbol'
|
|
173
|
+
prefix_reg = Regexp.new(msg['prefix'].sub(':', ''))
|
|
174
|
+
symbols = Symbol.all_symbols.lazy.select { |s| s.to_s =~ prefix_reg }
|
|
175
|
+
completions = symbols.map { |c| { 'candidate' => ":#{c}", 'type' => 'label' } } +
|
|
176
|
+
symbols.map { |c| { 'candidate' => "#{c}:", 'type' => 'label' } }
|
|
177
|
+
send_msg(response_for(msg, {
|
|
178
|
+
'status' => ['done'],
|
|
179
|
+
'completions' => completions.to_a.uniq
|
|
180
|
+
}))
|
|
181
|
+
return
|
|
182
|
+
when 'constant'
|
|
183
|
+
me = evaluate_code("self", msg)
|
|
184
|
+
wrap_all.(me.class.constants, 'constant') +
|
|
185
|
+
wrap_all.(Object.constants, 'constant')
|
|
186
|
+
when 'call'
|
|
187
|
+
if context['separator'] == '::'
|
|
188
|
+
wrap_all.(evaluate_code(context['callee'], msg).constants, 'constant')
|
|
189
|
+
else
|
|
190
|
+
methods.(evaluate_code(context['callee'], msg))
|
|
191
|
+
end
|
|
192
|
+
when 'identifier'
|
|
193
|
+
methods.(evaluate_code('self', msg)) +
|
|
194
|
+
wrap_all.(evaluate_code('binding.local_variables', msg), 'local')
|
|
195
|
+
when 'instance_variable'
|
|
196
|
+
wrap_all.(evaluate_code('instance_variables', msg), 'attribute/instance')
|
|
197
|
+
when 'class_variable'
|
|
198
|
+
me = evaluate_code('self', msg)
|
|
199
|
+
if me.instance_of?(Class)
|
|
200
|
+
wrap_all.(me.class_variables, 'attribute/class')
|
|
201
|
+
else
|
|
202
|
+
wrap_all.(me.class.class_variables, 'attribute/class')
|
|
203
|
+
end
|
|
204
|
+
when 'scope_resolution'
|
|
205
|
+
wrap_all.(evaluate_code(context['callee'], msg).constants, 'constant')
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
send_msg(response_for(msg, {
|
|
209
|
+
'status' => ['done'],
|
|
210
|
+
'completions' => completions.select { |x| x['candidate'] =~ prefix_reg }.to_a.uniq
|
|
211
|
+
}))
|
|
212
|
+
rescue Exception
|
|
213
|
+
send_msg(response_for(msg, {
|
|
214
|
+
'status' => ['done'],
|
|
215
|
+
'completions' => []
|
|
216
|
+
}))
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private def find_definition(msg)
|
|
220
|
+
# {
|
|
221
|
+
# "op" => "find_definition",
|
|
222
|
+
# "file" => "/tmp/some_file.rb",
|
|
223
|
+
# "line" => 7,
|
|
224
|
+
# "symbol" => "upcase",
|
|
225
|
+
# "id" => "d1",
|
|
226
|
+
# "context" => {"prefix" => "a_string"}
|
|
227
|
+
# }
|
|
228
|
+
prefix = msg.fetch('context', {}).fetch('prefix', 'self')
|
|
229
|
+
prefix_class = evaluate_code("#{prefix}.class", msg)
|
|
230
|
+
result = prefix_class.__lazuli_source_location(msg.fetch('symbol').intern)
|
|
231
|
+
|
|
232
|
+
if result.nil?
|
|
233
|
+
send_msg(response_for(msg, {
|
|
234
|
+
'status' => ['done', 'notfound']
|
|
235
|
+
}))
|
|
236
|
+
else
|
|
237
|
+
file, row = result
|
|
238
|
+
send_msg(response_for(msg, {
|
|
239
|
+
'file' => file,
|
|
240
|
+
'line' => row - 1,
|
|
241
|
+
'status' => ['done']
|
|
242
|
+
}))
|
|
243
|
+
end
|
|
244
|
+
rescue Exception => e
|
|
245
|
+
send_msg(response_for(msg, {
|
|
246
|
+
'status' => ['done', 'notfound']
|
|
247
|
+
}))
|
|
248
|
+
end
|
|
249
|
+
|
|
155
250
|
private def get_watches(msg)
|
|
156
251
|
msg['id'] ||= "eval_#{@counter += 1}"
|
|
157
252
|
file = msg['file']
|
|
@@ -213,15 +308,13 @@ module NREPL
|
|
|
213
308
|
code = code.gsub(/^NREPL\.stop!$/, "NREPL.stop_#{method_name}(binding)")
|
|
214
309
|
define_stop_function!(msg, method_name)
|
|
215
310
|
end
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
end
|
|
224
|
-
evaluate_code(code, msg['file'], msg['line'], original_bind)
|
|
311
|
+
begin
|
|
312
|
+
res = evaluate_code(code, msg)
|
|
313
|
+
[:result, res]
|
|
314
|
+
rescue Exception => e
|
|
315
|
+
raise e if e.is_a?(SyntaxError)
|
|
316
|
+
[:error, e.inspect.sub(/\>$/, ' stack=' + e.backtrace.inspect+'>')]
|
|
317
|
+
end.inspect
|
|
225
318
|
end
|
|
226
319
|
|
|
227
320
|
unless pending_eval[:stopped?]
|
|
@@ -229,10 +322,77 @@ module NREPL
|
|
|
229
322
|
end
|
|
230
323
|
end
|
|
231
324
|
|
|
232
|
-
private def
|
|
325
|
+
private def inspect_result(result)
|
|
326
|
+
return result.inspect
|
|
327
|
+
|
|
328
|
+
class_for_res = ->{ result.class.ancestors.find(&:name) }
|
|
329
|
+
case result
|
|
330
|
+
# when BigDecimal
|
|
331
|
+
# ["literal", "BigDecimal: #{result.to_f}"]
|
|
332
|
+
when String
|
|
333
|
+
["string", result]
|
|
334
|
+
when Numeric
|
|
335
|
+
["number", result.to_s]
|
|
336
|
+
when Hash
|
|
337
|
+
[
|
|
338
|
+
"map",
|
|
339
|
+
nil,
|
|
340
|
+
"{",
|
|
341
|
+
" => ",
|
|
342
|
+
", ",
|
|
343
|
+
"}",
|
|
344
|
+
result.map { |(k, v)| [inspect_result(k), inspect_result(v)] }
|
|
345
|
+
]
|
|
346
|
+
when Array
|
|
347
|
+
["coll", nil, "[", ", ", "]", result.map { |i| inspect_result(i) }]
|
|
348
|
+
# when Enumerable
|
|
349
|
+
# ["coll", class_for_res.().name, "[", ", ", "]", result.map { |i| inspect_result(i) }]
|
|
350
|
+
when Exception
|
|
351
|
+
[
|
|
352
|
+
"exception",
|
|
353
|
+
["literal", "#{class_for_res.().name}: #{result.message}"],
|
|
354
|
+
(result.backtrace_locations || []).map { |l| [l.path, l.lineno, nil, l.label] }
|
|
355
|
+
]
|
|
356
|
+
else
|
|
357
|
+
inspected = result.inspect
|
|
358
|
+
match = inspected.match(/#\<(.*)\>/)
|
|
359
|
+
if match
|
|
360
|
+
vars = match.captures[0]
|
|
361
|
+
inner_vars = vars.scan(/\w(?:[\w\d_]\??)+=/)
|
|
362
|
+
inners = inner_vars.flat_map do |v|
|
|
363
|
+
begin
|
|
364
|
+
v = v.chop
|
|
365
|
+
[[["literal", v], inspect_result(result.send(v))]]
|
|
366
|
+
rescue NoMethodError
|
|
367
|
+
[]
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
[
|
|
371
|
+
"object",
|
|
372
|
+
class_for_res.().name,
|
|
373
|
+
result.inspect,
|
|
374
|
+
inners
|
|
375
|
+
]
|
|
376
|
+
else
|
|
377
|
+
[ "literal", result.inspect ]
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
private def evaluate_code(code, msg)
|
|
383
|
+
bind = if msg['stop_id']
|
|
384
|
+
@pending_evals.fetch(msg['stop_id'], {})[:binding]
|
|
385
|
+
elsif msg['watch_id']
|
|
386
|
+
@bindings_by_id.fetch(msg['watch_id'], {})[:binding]
|
|
387
|
+
else
|
|
388
|
+
find_row_based_binding(msg) || @binding
|
|
389
|
+
end
|
|
390
|
+
file = msg['file'] || 'EVAL'
|
|
391
|
+
line = (msg['line'] || 0) + 1
|
|
392
|
+
# evaluate_code(code, msg['file'], msg['line'], original_bind)
|
|
233
393
|
bind ||= TOPLEVEL_BINDING.dup
|
|
234
|
-
line = line ? line + 1 : 1
|
|
235
|
-
eval(code, bind, file
|
|
394
|
+
# line = line ? line + 1 : 1
|
|
395
|
+
eval(code, bind, file, line)
|
|
236
396
|
end
|
|
237
397
|
|
|
238
398
|
private def find_row_based_binding(msg)
|
|
@@ -302,7 +462,8 @@ module NREPL
|
|
|
302
462
|
end
|
|
303
463
|
|
|
304
464
|
def send_exception(msg, e)
|
|
305
|
-
|
|
465
|
+
error = e.inspect.sub(/\>$/, ' stack=' + e.backtrace.inspect+'>')
|
|
466
|
+
send_msg(response_for(msg, { 'ex' => error, 'status' => ['done', 'error'] }))
|
|
306
467
|
end
|
|
307
468
|
|
|
308
469
|
def send_msg(msg)
|
data/lib/nrepl-lazuli/server.rb
CHANGED
|
@@ -8,6 +8,8 @@ require_relative 'bencode'
|
|
|
8
8
|
require_relative 'connection'
|
|
9
9
|
require_relative 'fake_stdout'
|
|
10
10
|
|
|
11
|
+
$pwd = Dir.pwd
|
|
12
|
+
|
|
11
13
|
module NREPL
|
|
12
14
|
class Server
|
|
13
15
|
attr_reader :debug, :port, :host, :call_trace
|
|
@@ -39,7 +41,7 @@ module NREPL
|
|
|
39
41
|
loader: nil
|
|
40
42
|
)
|
|
41
43
|
@port = port
|
|
42
|
-
|
|
44
|
+
$pwd = pwd
|
|
43
45
|
@host = host
|
|
44
46
|
@debug = debug
|
|
45
47
|
@connections = Set.new
|
|
@@ -107,11 +109,11 @@ module NREPL
|
|
|
107
109
|
end
|
|
108
110
|
|
|
109
111
|
def auto_create_bindings!
|
|
110
|
-
dir_regex = Regexp.new("^#{Regexp.escape(
|
|
112
|
+
dir_regex = Regexp.new("^#{Regexp.escape($pwd)}")
|
|
111
113
|
trace_proc = proc do |tp|
|
|
112
114
|
path = tp.path
|
|
113
115
|
next if tp.path =~ /^(\<|.eval)/
|
|
114
|
-
path = File.join(
|
|
116
|
+
path = File.join($pwd, path) if File.dirname(path) == '.'
|
|
115
117
|
id = "#{path}:#{tp.lineno}"
|
|
116
118
|
b = tp.binding
|
|
117
119
|
@bindings_by_id[id] = {binding: b, file: path, row: tp.lineno-1}
|
|
@@ -182,13 +184,6 @@ module NREPL
|
|
|
182
184
|
|
|
183
185
|
Thread.prepend(ThreadPatch)
|
|
184
186
|
|
|
185
|
-
# Also...
|
|
186
|
-
module MethodLocationFixer
|
|
187
|
-
def __lazuli_source_location
|
|
188
|
-
@__lazuli_source_location || source_location
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
187
|
module DefinitionFixer
|
|
193
188
|
@@definitions = {}
|
|
194
189
|
|
|
@@ -211,8 +206,8 @@ module NREPL
|
|
|
211
206
|
|
|
212
207
|
def method_added(method_name)
|
|
213
208
|
return if method_name == :__lazuli_source_location
|
|
214
|
-
|
|
215
|
-
path = caller.select { |x| x.start_with?(pwd) }[0]
|
|
209
|
+
|
|
210
|
+
path = caller.select { |x| x.start_with?($pwd) }[0]
|
|
216
211
|
if path
|
|
217
212
|
(file, row) = path.split(/:/)
|
|
218
213
|
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nrepl-lazuli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Maurício Szabo
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: A Ruby nREPL server, made to be used with Lazuli plug-in (but can be
|
|
13
13
|
used with any nREPL client too)
|
|
@@ -17,6 +17,8 @@ extensions: []
|
|
|
17
17
|
extra_rdoc_files: []
|
|
18
18
|
files:
|
|
19
19
|
- lib/nrepl-lazuli.rb
|
|
20
|
+
- lib/nrepl-lazuli/bencode.rb
|
|
21
|
+
- lib/nrepl-lazuli/client.rb
|
|
20
22
|
- lib/nrepl-lazuli/connection.rb
|
|
21
23
|
- lib/nrepl-lazuli/fake_stdout.rb
|
|
22
24
|
- lib/nrepl-lazuli/server.rb
|
|
@@ -39,7 +41,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
39
41
|
- !ruby/object:Gem::Version
|
|
40
42
|
version: '0'
|
|
41
43
|
requirements: []
|
|
42
|
-
rubygems_version:
|
|
44
|
+
rubygems_version: 4.0.6
|
|
43
45
|
specification_version: 4
|
|
44
46
|
summary: A Ruby nREPL server
|
|
45
47
|
test_files: []
|