neovim 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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