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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/CHANGELOG.md +10 -0
- data/lib/neovim/current.rb +19 -2
- data/lib/neovim/host.rb +3 -11
- data/lib/neovim/host/loader.rb +0 -2
- data/lib/neovim/line_range.rb +14 -2
- data/lib/neovim/plugin.rb +1 -4
- data/lib/neovim/plugin/dsl.rb +6 -14
- data/lib/neovim/plugin/handler.rb +2 -9
- data/lib/neovim/ruby_provider.rb +9 -15
- data/lib/neovim/session.rb +23 -70
- data/lib/neovim/session/api.rb +61 -0
- data/lib/neovim/session/event_loop.rb +108 -0
- data/lib/neovim/session/notification.rb +19 -0
- data/lib/neovim/session/request.rb +31 -0
- data/lib/neovim/session/rpc.rb +95 -0
- data/lib/neovim/session/serializer.rb +62 -0
- data/lib/neovim/version.rb +1 -1
- data/neovim.gemspec +1 -1
- data/spec/acceptance/neovim-ruby-host_spec.rb +0 -5
- data/spec/acceptance/ruby_provider_spec.rb +35 -3
- data/spec/helper.rb +33 -2
- data/spec/neovim/host_spec.rb +1 -1
- data/spec/neovim/plugin_spec.rb +2 -2
- data/spec/neovim/session/api_spec.rb +46 -0
- data/spec/neovim/session/event_loop_spec.rb +99 -0
- data/spec/neovim/session/rpc_spec.rb +108 -0
- data/spec/neovim/session/serializer_spec.rb +48 -0
- data/spec/neovim/session_spec.rb +1 -1
- metadata +16 -18
- data/lib/neovim/api.rb +0 -80
- data/lib/neovim/async_session.rb +0 -119
- data/lib/neovim/event_loop.rb +0 -128
- data/lib/neovim/msgpack_stream.rb +0 -80
- data/lib/neovim/notification.rb +0 -17
- data/lib/neovim/request.rb +0 -29
- data/spec/neovim/api_spec.rb +0 -44
- data/spec/neovim/async_session_spec.rb +0 -106
- data/spec/neovim/event_loop_spec.rb +0 -97
- data/spec/neovim/msgpack_stream_spec.rb +0 -46
- 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
|
data/lib/neovim/version.rb
CHANGED
data/neovim.gemspec
CHANGED
@@ -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 =
|
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(:@
|
60
|
-
nvim.eval("rpcrequest(host, 'ruby_execute', 'VIM.command(\"let g:
|
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("
|
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
|