neovim 0.2.5 → 0.3.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/CHANGELOG.md +10 -0
  4. data/lib/neovim/current.rb +19 -2
  5. data/lib/neovim/host.rb +3 -11
  6. data/lib/neovim/host/loader.rb +0 -2
  7. data/lib/neovim/line_range.rb +14 -2
  8. data/lib/neovim/plugin.rb +1 -4
  9. data/lib/neovim/plugin/dsl.rb +6 -14
  10. data/lib/neovim/plugin/handler.rb +2 -9
  11. data/lib/neovim/ruby_provider.rb +9 -15
  12. data/lib/neovim/session.rb +23 -70
  13. data/lib/neovim/session/api.rb +61 -0
  14. data/lib/neovim/session/event_loop.rb +108 -0
  15. data/lib/neovim/session/notification.rb +19 -0
  16. data/lib/neovim/session/request.rb +31 -0
  17. data/lib/neovim/session/rpc.rb +95 -0
  18. data/lib/neovim/session/serializer.rb +62 -0
  19. data/lib/neovim/version.rb +1 -1
  20. data/neovim.gemspec +1 -1
  21. data/spec/acceptance/neovim-ruby-host_spec.rb +0 -5
  22. data/spec/acceptance/ruby_provider_spec.rb +35 -3
  23. data/spec/helper.rb +33 -2
  24. data/spec/neovim/host_spec.rb +1 -1
  25. data/spec/neovim/plugin_spec.rb +2 -2
  26. data/spec/neovim/session/api_spec.rb +46 -0
  27. data/spec/neovim/session/event_loop_spec.rb +99 -0
  28. data/spec/neovim/session/rpc_spec.rb +108 -0
  29. data/spec/neovim/session/serializer_spec.rb +48 -0
  30. data/spec/neovim/session_spec.rb +1 -1
  31. metadata +16 -18
  32. data/lib/neovim/api.rb +0 -80
  33. data/lib/neovim/async_session.rb +0 -119
  34. data/lib/neovim/event_loop.rb +0 -128
  35. data/lib/neovim/msgpack_stream.rb +0 -80
  36. data/lib/neovim/notification.rb +0 -17
  37. data/lib/neovim/request.rb +0 -29
  38. data/spec/neovim/api_spec.rb +0 -44
  39. data/spec/neovim/async_session_spec.rb +0 -106
  40. data/spec/neovim/event_loop_spec.rb +0 -97
  41. data/spec/neovim/msgpack_stream_spec.rb +0 -46
  42. data/spec/support.rb +0 -33
@@ -0,0 +1,61 @@
1
+ module Neovim
2
+ class Session
3
+ # @api private
4
+ class API
5
+ attr_reader :channel_id
6
+
7
+ # Represents an unknown API. Used as a stand-in when the API hasn't been
8
+ # discovered yet via the +vim_get_api_info+ RPC call.
9
+ def self.null
10
+ new([nil, {"functions" => [], "types" => []}])
11
+ end
12
+
13
+ def initialize(payload)
14
+ @channel_id, @api_info = payload
15
+ end
16
+
17
+ # Return all functions defined by the API.
18
+ def functions
19
+ @functions ||= @api_info.fetch("functions").inject({}) do |acc, func|
20
+ name, async = func.values_at("name", "async")
21
+ acc.merge(name => Function.new(name, async))
22
+ end
23
+ end
24
+
25
+ # Return information about +nvim+ types. Used for registering MessagePack
26
+ # +ext+ types.
27
+ def types
28
+ @types ||= @api_info.fetch("types")
29
+ end
30
+
31
+ # Return a list of functions with the given name prefix.
32
+ def functions_with_prefix(prefix)
33
+ functions.inject([]) do |acc, (name, function)|
34
+ name =~ /\A#{prefix}/ ? acc.push(function) : acc
35
+ end
36
+ end
37
+
38
+ # Find a function with the given name.
39
+ def function(name)
40
+ functions[name.to_s]
41
+ end
42
+
43
+ # Truncate the output of inspect so console sessions are more pleasant.
44
+ def inspect
45
+ "#<#{self.class}:0x%x @types={...} @functions={...}>" % (object_id << 1)
46
+ end
47
+
48
+ class Function < Struct.new(:name, :async)
49
+ # Apply this function to a running RPC session. Sends either a request if
50
+ # +async+ is +false+ or a notification if +async+ is +true+.
51
+ def call(session, *args)
52
+ if async
53
+ session.notify(name, *args)
54
+ else
55
+ session.request(name, *args)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,108 @@
1
+ require "neovim/logging"
2
+ require "socket"
3
+
4
+ module Neovim
5
+ class Session
6
+ # The lowest level interface to reading from and writing to +nvim+.
7
+ #
8
+ # @api private
9
+ class EventLoop
10
+ include Logging
11
+
12
+ private_class_method :new
13
+
14
+ # Connect to a TCP socket.
15
+ def self.tcp(host, port)
16
+ socket = TCPSocket.new(host, port)
17
+ new(socket)
18
+ end
19
+
20
+ # Connect to a UNIX domain socket.
21
+ def self.unix(path)
22
+ socket = UNIXSocket.new(path)
23
+ new(socket)
24
+ end
25
+
26
+ # Spawn and connect to a child +nvim+ process.
27
+ def self.child(argv)
28
+ io = IO.popen(argv | ["--embed"], "rb+").tap do |_io|
29
+ Process.detach(_io.pid)
30
+ end
31
+
32
+ new(io)
33
+ end
34
+
35
+ # Connect to the current process's standard streams. This is used to
36
+ # promote the current process to a Ruby plugin host.
37
+ def self.stdio
38
+ new(STDIN, STDOUT)
39
+ end
40
+
41
+ def initialize(rd, wr=rd)
42
+ @rd, @wr = rd, wr
43
+ @running = false
44
+ end
45
+
46
+ # Write data to the underlying +IO+. This will block until all the
47
+ # data has been written.
48
+ def write(data)
49
+ start = 0
50
+ size = data.size
51
+ debug("writing #{data.inspect}")
52
+
53
+ begin
54
+ while start < size
55
+ start += @wr.write_nonblock(data[start..-1])
56
+ end
57
+ self
58
+ rescue IO::WaitWritable
59
+ IO.select(nil, [@wr], nil, 1)
60
+ retry
61
+ end
62
+ end
63
+
64
+ # Run the event loop, reading from the underlying +IO+ and yielding
65
+ # received messages to the block.
66
+ def run
67
+ @running = true
68
+
69
+ loop do
70
+ break unless @running
71
+ message = @rd.readpartial(1024 * 16)
72
+ debug("received #{message.inspect}")
73
+ yield message if block_given?
74
+ end
75
+ rescue EOFError
76
+ info("got EOFError")
77
+ rescue => e
78
+ fatal("got unexpected error #{e.inspect}")
79
+ debug(e.backtrace.join("\n"))
80
+ end
81
+
82
+ # Stop the event loop.
83
+ def stop
84
+ @running = false
85
+ end
86
+
87
+ # Stop the event loop and close underlying +IO+s.
88
+ def shutdown
89
+ stop
90
+
91
+ [@rd, @wr].each do |io|
92
+ begin
93
+ io.close
94
+ rescue IOError
95
+ end
96
+
97
+ begin
98
+ if pid = io.pid
99
+ Process.kill(:TERM, pid)
100
+ Process.waitpid(pid)
101
+ end
102
+ rescue IOError, Errno::ESRCH, Errno::ECHILD
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,19 @@
1
+ module Neovim
2
+ class Session
3
+ # An asynchronous message from +nvim+.
4
+ #
5
+ # @api private
6
+ class Notification
7
+ attr_reader :method_name, :arguments
8
+
9
+ def initialize(method_name, args)
10
+ @method_name = method_name.to_s
11
+ @arguments = args
12
+ end
13
+
14
+ def sync?
15
+ false
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ module Neovim
2
+ class Session
3
+ # A synchronous message from +nvim+.
4
+ #
5
+ # @api private
6
+ class Request
7
+ attr_reader :method_name, :arguments
8
+
9
+ def initialize(method_name, args, serializer, request_id)
10
+ @method_name = method_name.to_s
11
+ @arguments = args
12
+ @serializer = serializer
13
+ @request_id = request_id
14
+ end
15
+
16
+ def sync?
17
+ true
18
+ end
19
+
20
+ def respond(value)
21
+ @serializer.write([1, @request_id, nil, value])
22
+ self
23
+ end
24
+
25
+ def error(message)
26
+ @serializer.write([1, @request_id, message, nil])
27
+ self
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,95 @@
1
+ require "neovim/logging"
2
+ require "neovim/session/request"
3
+ require "neovim/session/notification"
4
+
5
+ module Neovim
6
+ class Session
7
+ # Handles formatting RPC requests and writing them to the +Serializer+.
8
+ # This exposes an asynchronous API, in which responses are handled in
9
+ # callbacks.
10
+ #
11
+ # @api private
12
+ class RPC
13
+ include Logging
14
+
15
+ attr_reader :serializer
16
+
17
+ def initialize(serializer)
18
+ @serializer = serializer
19
+ @request_id = 0
20
+ @pending_requests = {}
21
+ end
22
+
23
+ # Send an RPC request and enqueue it's callback to be called when a
24
+ # response is received.
25
+ def request(method, *args, &response_cb)
26
+ reqid = @request_id
27
+ @request_id += 1
28
+
29
+ @serializer.write([0, reqid, method, args])
30
+ @pending_requests[reqid] = response_cb || Proc.new {}
31
+ self
32
+ end
33
+
34
+ # Send an RPC notification. Notifications don't receive a response
35
+ # from +nvim+.
36
+ def notify(method, *args)
37
+ @serializer.write([2, method, args])
38
+ self
39
+ end
40
+
41
+ # Run the event loop, yielding received RPC messages to the block. RPC
42
+ # requests and notifications from +nvim+ will be wrapped in +Request+
43
+ # and +Notification+ objects, respectively, and responses will be
44
+ # passed to their callbacks with optional errors.
45
+ def run(&callback)
46
+ @serializer.run do |msg|
47
+ debug("received #{msg.inspect}")
48
+ kind, *payload = msg
49
+
50
+ case kind
51
+ when 0
52
+ handle_request(payload, callback)
53
+ when 1
54
+ handle_response(payload)
55
+ when 2
56
+ handle_notification(payload, callback)
57
+ end
58
+ end
59
+ rescue => e
60
+ fatal("got unexpected error #{e.inspect}")
61
+ debug(e.backtrace.join("\n"))
62
+ end
63
+
64
+ # Stop the event loop.
65
+ def stop
66
+ @serializer.stop
67
+ end
68
+
69
+ # Shut down the event loop.
70
+ def shutdown
71
+ @serializer.shutdown
72
+ end
73
+
74
+ private
75
+
76
+ def handle_request(payload, callback)
77
+ callback ||= Proc.new {}
78
+ reqid, method, args = payload
79
+ callback.call(Request.new(method, args, @serializer, reqid))
80
+ end
81
+
82
+ def handle_response(payload)
83
+ reqid, (_, error), result = payload
84
+ callback = @pending_requests.delete(reqid) || Proc.new {}
85
+ callback.call(error, result)
86
+ end
87
+
88
+ def handle_notification(payload, callback)
89
+ callback ||= Proc.new {}
90
+ method, args = payload
91
+ callback.call(Notification.new(method, args))
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,62 @@
1
+ require "neovim/logging"
2
+ require "msgpack"
3
+
4
+ module Neovim
5
+ class Session
6
+ # Handles serializing RPC messages to MessagePack and passing them to
7
+ # the event loop.
8
+ #
9
+ # @api private
10
+ class Serializer
11
+ include Logging
12
+
13
+ def initialize(event_loop)
14
+ @event_loop = event_loop
15
+ @unpacker = MessagePack::Unpacker.new
16
+ end
17
+
18
+ # Serialize an RPC message to and write it to the event loop.
19
+ def write(msg)
20
+ debug("writing #{msg.inspect}")
21
+ @event_loop.write(MessagePack.pack(msg))
22
+ self
23
+ end
24
+
25
+ # Run the event loop, yielding deserialized messages to the block.
26
+ def run
27
+ @event_loop.run do |data|
28
+ @unpacker.feed_each(data) do |msg|
29
+ debug("received #{msg.inspect}")
30
+ yield msg if block_given?
31
+ end
32
+ end
33
+ rescue => e
34
+ fatal("got unexpected error #{e.inspect}")
35
+ debug(e.backtrace.join("\n"))
36
+ end
37
+
38
+ # Stop the event loop.
39
+ def stop
40
+ @event_loop.stop
41
+ end
42
+
43
+ # Shut down the event loop.
44
+ def shutdown
45
+ @event_loop.shutdown
46
+ end
47
+
48
+ # Register msgpack ext types using the provided API and session
49
+ def register_types(api, session)
50
+ info("registering msgpack ext types")
51
+ api.types.each do |type, info|
52
+ klass = Neovim.const_get(type)
53
+ id = info.fetch("id")
54
+
55
+ @unpacker.register_type(id) do |data|
56
+ klass.new(MessagePack.unpack(data), session)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,3 +1,3 @@
1
1
  module Neovim
2
- VERSION = Gem::Version.new("0.2.5")
2
+ VERSION = Gem::Version.new("0.3.0")
3
3
  end
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Neovim::VERSION
9
9
  spec.authors = ["Alex Genco"]
10
10
  spec.email = ["alexgenco@gmail.com"]
11
- spec.summary = %q{A Ruby client for Neovim}
11
+ spec.summary = "A Ruby client for Neovim"
12
12
  spec.homepage = "https://github.com/alexgenco/neovim-ruby"
13
13
  spec.license = "MIT"
14
14
 
@@ -43,10 +43,6 @@ RSpec.describe "neovim-ruby-host" do
43
43
  plug.autocmd(:BufEnter, :pattern => "*.rb") do |nvim|
44
44
  nvim.current.line = "Ruby file, eh?"
45
45
  end
46
-
47
- plug.rpc(:TopLevelAdd, :nargs => 2, :sync => true) do |nvim, x, y|
48
- x + y
49
- end
50
46
  end
51
47
  RUBY
52
48
 
@@ -56,7 +52,6 @@ RSpec.describe "neovim-ruby-host" do
56
52
 
57
53
  expect(nvim.eval("rpcrequest(host, 'poll')")).to eq("ok")
58
54
  expect(nvim.eval("rpcrequest(host, '#{plugin_path}:function:SyncAdd', [1, 2])")).to eq(3)
59
- expect(nvim.eval("rpcrequest(host, 'TopLevelAdd', 1, 2)")).to eq(3)
60
55
 
61
56
  expect {
62
57
  nvim.command("call rpcnotify(host, '#{plugin_path}:autocmd:BufEnter:*.rb')")
@@ -56,10 +56,10 @@ RSpec.describe "ruby_provider" do
56
56
 
57
57
  expect(nvim.get_var("foo")).to be(123)
58
58
 
59
- nvim.eval("rpcrequest(host, 'ruby_execute', '$curwin.instance_variable_set(:@foo, 456)')")
60
- nvim.eval("rpcrequest(host, 'ruby_execute', 'VIM.command(\"let g:foo = \#{$curwin.instance_variable_get(:@foo)}\")')")
59
+ nvim.eval("rpcrequest(host, 'ruby_execute', '$curwin.instance_variable_set(:@bar, 456)')")
60
+ nvim.eval("rpcrequest(host, 'ruby_execute', 'VIM.command(\"let g:bar = \#{$curwin.instance_variable_get(:@bar)}\")')")
61
61
 
62
- expect(nvim.get_var("foo")).to be(456)
62
+ expect(nvim.get_var("bar")).to be(456)
63
63
  end
64
64
  end
65
65
 
@@ -108,6 +108,38 @@ RSpec.describe "ruby_provider" do
108
108
  nvim.eval("rpcrequest(host, 'ruby_execute_file', '#{script_path}')")
109
109
  expect(nvim.get_var("called")).to be(2)
110
110
  end
111
+
112
+ it "persists instance state in globals" do
113
+ File.write(script_path, <<-RUBY)
114
+ def $curbuf.foo
115
+ @foo ||= 0
116
+ @foo += 1
117
+ end
118
+
119
+ VIM.command("let g:foo = \#{$curbuf.foo}")
120
+ RUBY
121
+
122
+ nvim.eval("rpcrequest(host, 'ruby_execute_file', '#{script_path}')")
123
+ expect(nvim.get_var("foo")).to be(1)
124
+
125
+ nvim.eval("rpcrequest(host, 'ruby_execute_file', '#{script_path}')")
126
+ expect(nvim.get_var("foo")).to be(2)
127
+
128
+ File.write(script_path, <<-RUBY)
129
+ def $curwin.bar
130
+ @bar ||= 0
131
+ @bar += 1
132
+ end
133
+
134
+ VIM.command("let g:bar = \#{$curwin.bar}")
135
+ RUBY
136
+
137
+ nvim.eval("rpcrequest(host, 'ruby_execute_file', '#{script_path}')")
138
+ expect(nvim.get_var("bar")).to be(1)
139
+
140
+ nvim.eval("rpcrequest(host, 'ruby_execute_file', '#{script_path}')")
141
+ expect(nvim.get_var("bar")).to be(2)
142
+ end
111
143
  end
112
144
 
113
145
  describe "ruby_do_range" do