Skip to content

Commit e3df69f

Browse files
committed
Extract RunnerClient interface
1 parent 180ca75 commit e3df69f

File tree

6 files changed

+117
-84
lines changed

6 files changed

+117
-84
lines changed

lib/ruby_lsp/ruby_lsp_rails/addon.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ def initialize
2525

2626
# We first initialize the client as a NullClient, so that we can start the server in a background thread. Until
2727
# the real client is initialized, features that depend on it will not be blocked by using the NullClient
28-
@client = T.let(NullClient.new, RunnerClient)
28+
@client = T.let(NullClient.new, Client)
2929
end
3030

3131
sig { override.params(global_state: GlobalState, message_queue: Thread::Queue).void }
3232
def activate(global_state, message_queue)
3333
@global_state = T.let(global_state, T.nilable(RubyLsp::GlobalState))
3434
$stderr.puts("Activating Ruby LSP Rails addon v#{VERSION}")
3535
# Start booting the real client in a background thread. Until this completes, the client will be a NullClient
36-
Thread.new { @client = RunnerClient.create_client }
36+
Thread.new { @client = Client.create_client }
3737
register_additional_file_watchers(global_state: global_state, message_queue: message_queue)
3838
end
3939

lib/ruby_lsp/ruby_lsp_rails/code_lens.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class CodeLens
7878

7979
sig do
8080
params(
81-
client: RunnerClient,
81+
client: Client,
8282
global_state: GlobalState,
8383
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
8484
uri: URI::Generic,

lib/ruby_lsp/ruby_lsp_rails/definition.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Definition
3333

3434
sig do
3535
params(
36-
client: RunnerClient,
36+
client: Client,
3737
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
3838
node_context: NodeContext,
3939
index: RubyIndexer::Index,

lib/ruby_lsp/ruby_lsp_rails/hover.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Hover
2222

2323
sig do
2424
params(
25-
client: RunnerClient,
25+
client: Client,
2626
response_builder: ResponseBuilders::Hover,
2727
node_context: NodeContext,
2828
global_state: GlobalState,

lib/ruby_lsp/ruby_lsp_rails/runner_client.rb

Lines changed: 109 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@
66

77
module RubyLsp
88
module Rails
9-
class RunnerClient
9+
module Client
10+
extend T::Sig
11+
extend T::Helpers
12+
13+
abstract!
14+
1015
class << self
1116
extend T::Sig
1217

13-
sig { returns(RunnerClient) }
18+
sig { returns(Client) }
1419
def create_client
1520
if File.exist?("bin/rails")
16-
new
21+
RunnerClient.new
1722
else
1823
$stderr.puts(<<~MSG)
1924
Ruby LSP Rails failed to locate bin/rails in the current directory: #{Dir.pwd}"
@@ -32,6 +37,94 @@ class InitializationError < StandardError; end
3237
class IncompleteMessageError < StandardError; end
3338
class EmptyMessageError < StandardError; end
3439

40+
sig { overridable.params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
41+
def model(name)
42+
make_request("model", name: name)
43+
rescue IncompleteMessageError
44+
$stderr.puts("Ruby LSP Rails failed to get model information: #{read_error}")
45+
nil
46+
end
47+
48+
sig { overridable.params(controller: String, action: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
49+
def route(controller:, action:)
50+
make_request("route_info", controller: controller, action: action)
51+
rescue IncompleteMessageError
52+
$stderr.puts("Ruby LSP Rails failed to get route information: #{read_error}")
53+
nil
54+
end
55+
56+
sig { overridable.void }
57+
def trigger_reload
58+
$stderr.puts("Reloading Rails application")
59+
send_notification("reload")
60+
rescue IncompleteMessageError
61+
$stderr.puts("Ruby LSP Rails failed to trigger reload")
62+
nil
63+
end
64+
65+
sig do
66+
params(
67+
model_name: String,
68+
association_name: String,
69+
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
70+
end
71+
def association_target_location(model_name:, association_name:)
72+
make_request(
73+
"association_target_location",
74+
model_name: model_name,
75+
association_name: association_name,
76+
)
77+
rescue => e
78+
$stderr.puts("Ruby LSP Rails failed with #{e.message}: #{read_error}")
79+
end
80+
81+
sig { overridable.params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
82+
def route_location(name)
83+
make_request("route_location", name: name)
84+
rescue IncompleteMessageError
85+
$stderr.puts("Ruby LSP Rails failed to get route location: #{read_error}")
86+
nil
87+
end
88+
89+
sig { abstract.void }
90+
def shutdown; end
91+
92+
sig { abstract.returns(T::Boolean) }
93+
def stopped?; end
94+
95+
sig { abstract.returns(String) }
96+
def rails_root; end
97+
98+
private
99+
100+
sig do
101+
overridable.params(
102+
request: String,
103+
params: T.nilable(T::Hash[Symbol, T.untyped]),
104+
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
105+
end
106+
def make_request(request, params = nil)
107+
send_message(request, params)
108+
read_response
109+
end
110+
111+
sig { abstract.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
112+
def send_message(request, params = nil); end
113+
114+
# Notifications are like messages, but one-way, with no response sent back.
115+
sig { params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
116+
def send_notification(request, params = nil) = send_message(request, params)
117+
118+
sig { abstract.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
119+
def read_response; end
120+
121+
sig { abstract.returns(String) }
122+
def read_error; end
123+
end
124+
125+
class RunnerClient
126+
include Client
127+
35128
MAX_RETRIES = 5
36129

37130
extend T::Sig
@@ -89,59 +182,10 @@ def initialize
89182
end
90183
end
91184
rescue Errno::EPIPE, IncompleteMessageError
92-
raise InitializationError, @stderr.read
185+
raise InitializationError, read_error
93186
end
94187

95-
sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
96-
def model(name)
97-
make_request("model", name: name)
98-
rescue IncompleteMessageError
99-
$stderr.puts("Ruby LSP Rails failed to get model information: #{@stderr.read}")
100-
nil
101-
end
102-
103-
sig do
104-
params(
105-
model_name: String,
106-
association_name: String,
107-
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
108-
end
109-
def association_target_location(model_name:, association_name:)
110-
make_request(
111-
"association_target_location",
112-
model_name: model_name,
113-
association_name: association_name,
114-
)
115-
rescue => e
116-
$stderr.puts("Ruby LSP Rails failed with #{e.message}: #{@stderr.read}")
117-
end
118-
119-
sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
120-
def route_location(name)
121-
make_request("route_location", name: name)
122-
rescue IncompleteMessageError
123-
$stderr.puts("Ruby LSP Rails failed to get route location: #{@stderr.read}")
124-
nil
125-
end
126-
127-
sig { params(controller: String, action: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
128-
def route(controller:, action:)
129-
make_request("route_info", controller: controller, action: action)
130-
rescue IncompleteMessageError
131-
$stderr.puts("Ruby LSP Rails failed to get route information: #{@stderr.read}")
132-
nil
133-
end
134-
135-
sig { void }
136-
def trigger_reload
137-
$stderr.puts("Reloading Rails application")
138-
send_notification("reload")
139-
rescue IncompleteMessageError
140-
$stderr.puts("Ruby LSP Rails failed to trigger reload")
141-
nil
142-
end
143-
144-
sig { void }
188+
sig { override.void }
145189
def shutdown
146190
$stderr.puts("Ruby LSP Rails shutting down server")
147191
send_message("shutdown")
@@ -152,25 +196,14 @@ def shutdown
152196
force_kill
153197
end
154198

155-
sig { returns(T::Boolean) }
199+
sig { override.returns(T::Boolean) }
156200
def stopped?
157201
[@stdin, @stdout, @stderr].all?(&:closed?) && !@wait_thread.alive?
158202
end
159203

160204
private
161205

162-
sig do
163-
params(
164-
request: String,
165-
params: T.nilable(T::Hash[Symbol, T.untyped]),
166-
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
167-
end
168-
def make_request(request, params = nil)
169-
send_message(request, params)
170-
read_response
171-
end
172-
173-
sig { overridable.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
206+
sig { override.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
174207
def send_message(request, params = nil)
175208
message = { method: request }
176209
message[:params] = params if params
@@ -181,11 +214,7 @@ def send_message(request, params = nil)
181214
# The server connection died
182215
end
183216

184-
# Notifications are like messages, but one-way, with no response sent back.
185-
sig { params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
186-
def send_notification(request, params = nil) = send_message(request, params)
187-
188-
sig { overridable.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
217+
sig { override.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
189218
def read_response
190219
headers = @stdout.gets("\r\n\r\n")
191220
raise IncompleteMessageError unless headers
@@ -207,19 +236,20 @@ def read_response
207236
nil
208237
end
209238

239+
sig { override.returns(String) }
240+
def read_error = @stderr.read
241+
210242
sig { void }
211243
def force_kill
212244
# Windows does not support the `TERM` signal, so we're forced to use `KILL` here
213245
Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
214246
end
215247
end
216248

217-
class NullClient < RunnerClient
249+
class NullClient
218250
extend T::Sig
219251

220-
sig { void }
221-
def initialize # rubocop:disable Lint/MissingSuper
222-
end
252+
include Client
223253

224254
sig { override.void }
225255
def shutdown
@@ -247,6 +277,9 @@ def send_message(request, params = nil)
247277
def read_response
248278
# no-op
249279
end
280+
281+
sig { override.returns(String) }
282+
def read_error = "<no error information available>"
250283
end
251284
end
252285
end

test/ruby_lsp_rails/runner_client_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class RunnerClientTest < ActiveSupport::TestCase
4545
FileUtils.mv("bin/rails", "bin/rails_backup")
4646

4747
assert_output("", %r{Ruby LSP Rails failed to locate bin/rails in the current directory}) do
48-
client = RunnerClient.create_client
48+
client = Client.create_client
4949

5050
assert_instance_of(NullClient, client)
5151
assert_nil(client.model("User"))
@@ -61,7 +61,7 @@ class RunnerClientTest < ActiveSupport::TestCase
6161
"",
6262
/Ruby LSP Rails failed to initialize server/,
6363
) do
64-
client = RunnerClient.create_client
64+
client = Client.create_client
6565

6666
assert_instance_of(NullClient, client)
6767
assert_nil(client.model("User"))
@@ -78,7 +78,7 @@ class RunnerClientTest < ActiveSupport::TestCase
7878
File.write("test/dummy/config/application.rb", content + junk)
7979

8080
capture_subprocess_io do
81-
client = RunnerClient.create_client
81+
client = Client.create_client
8282

8383
response = T.must(client.model("User"))
8484
assert(response.key?(:columns))

0 commit comments

Comments
 (0)