Skip to content

Commit 97a7cad

Browse files
committed
WIP
1 parent b47335b commit 97a7cad

File tree

8 files changed

+726
-598
lines changed

8 files changed

+726
-598
lines changed

Gemfile

+2
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ platforms :mingw, :x64_mingw, :mswin, :jruby do
2525
gem "tzinfo"
2626
gem "tzinfo-data"
2727
end
28+
29+
gem "ruby-lsp", github: "Shopify/ruby-lsp", branch: "add-on-client-server-framework"

Gemfile.lock

+19-18
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ GIT
66
rdoc (6.6.3.1)
77
psych (>= 4.0.0)
88

9+
GIT
10+
remote: https://github.com/Shopify/ruby-lsp.git
11+
revision: dcb22575770cb200e812bdc0355805a6078d4069
12+
branch: add-on-client-server-framework
13+
specs:
14+
ruby-lsp (0.17.18)
15+
language_server-protocol (~> 3.17.0)
16+
prism (~> 1.0)
17+
rbs (>= 3, < 4)
18+
sorbet-runtime (>= 0.5.10782)
19+
920
PATH
1021
remote: .
1122
specs:
@@ -118,7 +129,7 @@ GEM
118129
reline (>= 0.4.2)
119130
json (2.7.2)
120131
language_server-protocol (3.17.0.3)
121-
logger (1.6.0)
132+
logger (1.6.1)
122133
loofah (2.22.0)
123134
crass (~> 1.0.2)
124135
nokogiri (>= 1.12.0)
@@ -146,8 +157,6 @@ GEM
146157
nio4r (2.7.3)
147158
nokogiri (1.16.5-arm64-darwin)
148159
racc (~> 1.4)
149-
nokogiri (1.16.5-x64-mingw-ucrt)
150-
racc (~> 1.4)
151160
nokogiri (1.16.5-x86_64-darwin)
152161
racc (~> 1.4)
153162
nokogiri (1.16.5-x86_64-linux)
@@ -156,7 +165,7 @@ GEM
156165
parser (3.3.1.0)
157166
ast (~> 2.4.1)
158167
racc
159-
prism (0.30.0)
168+
prism (1.0.0)
160169
psych (5.1.2)
161170
stringio
162171
public_suffix (5.0.5)
@@ -202,10 +211,10 @@ GEM
202211
zeitwerk (~> 2.6)
203212
rainbow (3.1.1)
204213
rake (13.2.1)
205-
rbi (0.1.13)
206-
prism (>= 0.18.0, < 1.0.0)
214+
rbi (0.2.0)
215+
prism (~> 1.0)
207216
sorbet-runtime (>= 0.5.9204)
208-
rbs (3.5.2)
217+
rbs (3.5.3)
209218
logger
210219
regexp_parser (2.9.0)
211220
reline (0.5.7)
@@ -234,11 +243,6 @@ GEM
234243
rubocop (~> 1.51)
235244
rubocop-sorbet (0.8.3)
236245
rubocop (>= 0.90.0)
237-
ruby-lsp (0.17.12)
238-
language_server-protocol (~> 3.17.0)
239-
prism (>= 0.29.0, < 0.31)
240-
rbs (>= 3, < 4)
241-
sorbet-runtime (>= 0.5.10782)
242246
ruby-progressbar (1.13.0)
243247
ruby2_keywords (0.0.5)
244248
sorbet (0.5.11406)
@@ -255,16 +259,15 @@ GEM
255259
sorbet-static-and-runtime (>= 0.5.10187)
256260
thor (>= 0.19.2)
257261
sqlite3 (1.7.3-arm64-darwin)
258-
sqlite3 (1.7.3-x64-mingw-ucrt)
259262
sqlite3 (1.7.3-x86_64-darwin)
260263
sqlite3 (1.7.3-x86_64-linux)
261264
stringio (3.1.0)
262265
strscan (3.1.0)
263-
tapioca (0.13.3)
266+
tapioca (0.16.2)
264267
bundler (>= 2.2.25)
265268
netrc (>= 0.11.0)
266269
parallel (>= 1.21.0)
267-
rbi (>= 0.1.4, < 0.2)
270+
rbi (~> 0.2)
268271
sorbet-static-and-runtime (>= 0.5.11087)
269272
spoom (>= 1.2.0)
270273
thor (>= 1.2.0)
@@ -273,8 +276,6 @@ GEM
273276
timeout (0.4.1)
274277
tzinfo (2.0.6)
275278
concurrent-ruby (~> 1.0)
276-
tzinfo-data (1.2024.1)
277-
tzinfo (>= 1.0.0)
278279
unicode-display_width (2.5.0)
279280
webmock (3.23.1)
280281
addressable (>= 2.8.0)
@@ -292,7 +293,6 @@ GEM
292293

293294
PLATFORMS
294295
arm64-darwin
295-
x64-mingw-ucrt
296296
x86_64-darwin
297297
x86_64-linux
298298

@@ -307,6 +307,7 @@ DEPENDENCIES
307307
rubocop-rake (~> 0.6.0)
308308
rubocop-shopify (~> 2.15)
309309
rubocop-sorbet (~> 0.8)
310+
ruby-lsp!
310311
ruby-lsp-rails!
311312
sorbet-static-and-runtime
312313
sqlite3 (< 2)

lib/ruby_lsp/ruby_lsp_rails/addon.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ def initialize
3636
sig { override.params(global_state: GlobalState, message_queue: Thread::Queue).void }
3737
def activate(global_state, message_queue)
3838
@global_state = global_state
39-
$stderr.puts("Activating Ruby LSP Rails addon v#{VERSION}")
39+
$stderr.puts("Activating Ruby LSP Rails addon v#{VERSION}") unless ENV["RAILS_ENV"] == "test"
4040
# Start booting the real client in a background thread. Until this completes, the client will be a NullClient
41-
Thread.new { @rails_runner_client = RunnerClient.create_client }
41+
Thread.new { @rails_runner_client = RunnerClient.create_client(self) }
4242
register_additional_file_watchers(global_state: global_state, message_queue: message_queue)
4343

4444
@global_state.index.register_enhancement(IndexingEnhancement.new)

lib/ruby_lsp/ruby_lsp_rails/runner_client.rb

+37-140
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@
33

44
require "json"
55
require "open3"
6+
require "ruby_lsp/addon/process_client"
67

78
module RubyLsp
89
module Rails
9-
class RunnerClient
10+
class RunnerClient < RubyLsp::Addon::ProcessClient
11+
COMMAND = T.let(["bundle", "exec", "rails", "runner", "#{__dir__}/server.rb", "start"].join(" "), String)
12+
1013
class << self
1114
extend T::Sig
1215

13-
sig { returns(RunnerClient) }
14-
def create_client
16+
sig { params(addon: RubyLsp::Addon).returns(RunnerClient) }
17+
def create_client(addon)
1518
if File.exist?("bin/rails")
16-
new
19+
new(addon, COMMAND)
1720
else
1821
$stderr.puts(<<~MSG)
1922
Ruby LSP Rails failed to locate bin/rails in the current directory: #{Dir.pwd}"
@@ -28,79 +31,43 @@ def create_client
2831
end
2932
end
3033

31-
class InitializationError < StandardError; end
32-
class IncompleteMessageError < StandardError; end
33-
class EmptyMessageError < StandardError; end
34-
35-
MAX_RETRIES = 5
36-
3734
extend T::Sig
3835

3936
sig { returns(String) }
40-
attr_reader :rails_root
41-
42-
sig { void }
43-
def initialize
44-
@mutex = T.let(Mutex.new, Mutex)
45-
# Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
46-
# parent ends, the spring process ends as well. If this is not set, Spring will throw an error while trying to
47-
# set its own session ID
48-
begin
49-
Process.setpgrp
50-
Process.setsid
51-
rescue Errno::EPERM
52-
# If we can't set the session ID, continue
53-
rescue NotImplementedError
54-
# setpgrp() may be unimplemented on some platform
55-
# https://github.com/Shopify/ruby-lsp-rails/issues/348
56-
end
57-
58-
stdin, stdout, stderr, wait_thread = Bundler.with_original_env do
59-
Open3.popen3("bundle", "exec", "rails", "runner", "#{__dir__}/server.rb", "start")
60-
end
61-
62-
@stdin = T.let(stdin, IO)
63-
@stdout = T.let(stdout, IO)
64-
@stderr = T.let(stderr, IO)
65-
@wait_thread = T.let(wait_thread, Process::Waiter)
66-
67-
# We set binmode for Windows compatibility
68-
@stdin.binmode
69-
@stdout.binmode
70-
@stderr.binmode
71-
72-
$stderr.puts("Ruby LSP Rails booting server")
73-
count = 0
37+
def rails_root
38+
T.must(@rails_root)
39+
end
7440

75-
begin
76-
count += 1
77-
initialize_response = T.must(read_response)
78-
@rails_root = T.let(initialize_response[:root], String)
79-
rescue EmptyMessageError
80-
$stderr.puts("Ruby LSP Rails is retrying initialize (#{count})")
81-
retry if count < MAX_RETRIES
41+
sig { params(message: String).void }
42+
def log_output(message)
43+
# We don't want to log output in tests
44+
unless ENV["RAILS_ENV"] == "test"
45+
super
8246
end
47+
end
48+
sig { override.params(response: T::Hash[Symbol, T.untyped]).void }
49+
def handle_initialize_response(response)
50+
@rails_root = T.let(response[:root], T.nilable(String))
51+
end
8352

84-
$stderr.puts("Finished booting Ruby LSP Rails server")
85-
53+
sig { override.void }
54+
def register_exit_handler
8655
unless ENV["RAILS_ENV"] == "test"
8756
at_exit do
88-
if @wait_thread.alive?
89-
$stderr.puts("Ruby LSP Rails is force killing the server")
57+
if wait_thread.alive?
58+
log_output("force killing the server")
9059
sleep(0.5) # give the server a bit of time if we already issued a shutdown notification
9160
force_kill
9261
end
9362
end
9463
end
95-
rescue Errno::EPIPE, IncompleteMessageError
96-
raise InitializationError, @stderr.read
9764
end
9865

9966
sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
10067
def model(name)
10168
make_request("model", name: name)
10269
rescue IncompleteMessageError
103-
$stderr.puts("Ruby LSP Rails failed to get model information: #{@stderr.read}")
70+
log_output("failed to get model information: #{stderr.read}")
10471
nil
10572
end
10673

@@ -117,117 +84,47 @@ def association_target_location(model_name:, association_name:)
11784
association_name: association_name,
11885
)
11986
rescue => e
120-
$stderr.puts("Ruby LSP Rails failed with #{e.message}: #{@stderr.read}")
87+
log_output("failed with #{e.message}: #{stderr.read}")
88+
nil
12189
end
12290

12391
sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
12492
def route_location(name)
12593
make_request("route_location", name: name)
12694
rescue IncompleteMessageError
127-
$stderr.puts("Ruby LSP Rails failed to get route location: #{@stderr.read}")
95+
log_output("failed to get route location: #{stderr.read}")
12896
nil
12997
end
13098

13199
sig { params(controller: String, action: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
132100
def route(controller:, action:)
133101
make_request("route_info", controller: controller, action: action)
134102
rescue IncompleteMessageError
135-
$stderr.puts("Ruby LSP Rails failed to get route information: #{@stderr.read}")
103+
log_output("failed to get route information: #{stderr.read}")
136104
nil
137105
end
138106

139107
sig { void }
140108
def trigger_reload
141-
$stderr.puts("Reloading Rails application")
109+
log_output("triggering reload")
142110
send_notification("reload")
143111
rescue IncompleteMessageError
144-
$stderr.puts("Ruby LSP Rails failed to trigger reload")
145-
nil
146-
end
147-
148-
sig { void }
149-
def shutdown
150-
$stderr.puts("Ruby LSP Rails shutting down server")
151-
send_message("shutdown")
152-
sleep(0.5) # give the server a bit of time to shutdown
153-
[@stdin, @stdout, @stderr].each(&:close)
154-
rescue IOError
155-
# The server connection may have died
156-
force_kill
157-
end
158-
159-
sig { returns(T::Boolean) }
160-
def stopped?
161-
[@stdin, @stdout, @stderr].all?(&:closed?) && !@wait_thread.alive?
162-
end
163-
164-
sig do
165-
params(
166-
request: String,
167-
params: T.nilable(T::Hash[Symbol, T.untyped]),
168-
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
169-
end
170-
def make_request(request, params = nil)
171-
send_message(request, params)
172-
read_response
173-
end
174-
175-
# Notifications are like messages, but one-way, with no response sent back.
176-
sig { params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
177-
def send_notification(request, params = nil) = send_message(request, params)
178-
179-
private
180-
181-
sig { overridable.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
182-
def send_message(request, params = nil)
183-
message = { method: request }
184-
message[:params] = params if params
185-
json = message.to_json
186-
187-
@mutex.synchronize do
188-
@stdin.write("Content-Length: #{json.length}\r\n\r\n", json)
189-
end
190-
rescue Errno::EPIPE
191-
# The server connection died
192-
end
193-
194-
sig { overridable.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
195-
def read_response
196-
raw_response = @mutex.synchronize do
197-
headers = @stdout.gets("\r\n\r\n")
198-
raise IncompleteMessageError unless headers
199-
200-
content_length = headers[/Content-Length: (\d+)/i, 1].to_i
201-
raise EmptyMessageError if content_length.zero?
202-
203-
@stdout.read(content_length)
204-
end
205-
206-
response = JSON.parse(T.must(raw_response), symbolize_names: true)
207-
208-
if response[:error]
209-
$stderr.puts("Ruby LSP Rails error: " + response[:error])
210-
return
211-
end
212-
213-
response.fetch(:result)
214-
rescue Errno::EPIPE
215-
# The server connection died
112+
log_output("failed to trigger reload")
216113
nil
217114
end
218-
219-
sig { void }
220-
def force_kill
221-
# Windows does not support the `TERM` signal, so we're forced to use `KILL` here
222-
Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
223-
end
224115
end
225116

226117
class NullClient < RunnerClient
227118
extend T::Sig
228119

229120
sig { void }
230121
def initialize # rubocop:disable Lint/MissingSuper
122+
# no-op
123+
end
124+
125+
sig { override.params(response: T::Hash[Symbol, T.untyped]).void }
126+
def handle_initialize_response(response)
127+
# no-op
231128
end
232129

233130
sig { override.void }

0 commit comments

Comments
 (0)