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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e7940230b6c771925c5ee557cecd7a98e043cbe94fed89efe8dee2ce77611c5
4
- data.tar.gz: e36fda8c9c90cdb098e8e5e8307ce487cb28776d3c0de28e075964fea7acb4ed
3
+ metadata.gz: 850adfc6733e86d502ffd88241f4dff665e34870c6c2a7d6a436ee77685565e8
4
+ data.tar.gz: 50d6783deae1558218fc67a1d47590c2e92d07f964daa7038d3b6f51aec2d7dc
5
5
  SHA512:
6
- metadata.gz: e7a121b406ed724c1c433cd9a854f6082d1628b0557ebc9fb9bbd6f9e6c8a26c2f9c0390708f954ef3154b107d5234d7a332e343911426585c7fb9ec82b35247
7
- data.tar.gz: e183f6dc2766a16297f27fbf08b7c164b9b43e8c88b753fff43bedb96b1a3ef2b7649187a53703a6d591e18c6c1d3fc7d578f7af9ac4b5b39b87c861b392f490
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
- original_bind = if msg['stop_id']
218
- @pending_evals.fetch(msg['stop_id'], {})[:binding]
219
- elsif msg['watch_id']
220
- @bindings_by_id.fetch(msg['watch_id'], {})[:binding]
221
- else
222
- find_row_based_binding(msg) || @binding
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 evaluate_code(code, file, line, bind)
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 || "EVAL", line).inspect
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
- send_msg(response_for(msg, { 'ex' => e.message, 'status' => ['done', 'error'] }))
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)
@@ -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
- @pwd = pwd
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(@pwd)}")
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(@pwd, path) if File.dirname(path) == '.'
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
- pwd = Dir.pwd
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.5.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: 2025-04-04 00:00:00.000000000 Z
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: 3.6.2
44
+ rubygems_version: 4.0.6
43
45
  specification_version: 4
44
46
  summary: A Ruby nREPL server
45
47
  test_files: []