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