ruby-lsp-rails 0.3.31 → 0.4.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/lib/ruby_lsp/ruby_lsp_rails/addon.rb +22 -5
- data/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb +57 -11
- data/lib/ruby_lsp/ruby_lsp_rails/server.rb +142 -17
- data/lib/ruby_lsp_rails/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6995848edfaf04a9ff896c3e5b26704ff27e32e459154da3cf5d1340acb032b3
|
|
4
|
+
data.tar.gz: 9af25b875fe37dab802e813a7018bef86cc9b5cad0163d8d2d7031f40083779f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b1cffaabe1cfe7db5c063d79cefdedaf4c00f79d9f1af561a8dc45976e8b731b750642722cdd89a84891e28f721e8074f86a58934e2e728e60863f2122ba7a4
|
|
7
|
+
data.tar.gz: 617030501d5184109d42400bed761b213dcc8b66ccd9528bae1ab795c2f96e220710de3df94e4425e9e76c4a9584514bff67e220c8bdae27004b1fc001724165
|
|
@@ -32,6 +32,12 @@ module RubyLsp
|
|
|
32
32
|
@rails_runner_client = T.let(NullClient.new, RunnerClient)
|
|
33
33
|
@global_state = T.let(nil, T.nilable(GlobalState))
|
|
34
34
|
@outgoing_queue = T.let(nil, T.nilable(Thread::Queue))
|
|
35
|
+
@settings = T.let(
|
|
36
|
+
{
|
|
37
|
+
enablePendingMigrationsPrompt: true,
|
|
38
|
+
},
|
|
39
|
+
T::Hash[Symbol, T.untyped],
|
|
40
|
+
)
|
|
35
41
|
@addon_mutex = T.let(Mutex.new, Mutex)
|
|
36
42
|
@client_mutex = T.let(Mutex.new, Mutex)
|
|
37
43
|
@client_mutex.lock
|
|
@@ -39,7 +45,9 @@ module RubyLsp
|
|
|
39
45
|
Thread.new do
|
|
40
46
|
@addon_mutex.synchronize do
|
|
41
47
|
# We need to ensure the Rails client is fully loaded before we activate the server addons
|
|
42
|
-
@client_mutex.synchronize
|
|
48
|
+
@client_mutex.synchronize do
|
|
49
|
+
@rails_runner_client = RunnerClient.create_client(T.must(@outgoing_queue), T.must(@global_state))
|
|
50
|
+
end
|
|
43
51
|
offer_to_run_pending_migrations
|
|
44
52
|
end
|
|
45
53
|
end
|
|
@@ -56,6 +64,9 @@ module RubyLsp
|
|
|
56
64
|
@outgoing_queue = outgoing_queue
|
|
57
65
|
@outgoing_queue << Notification.window_log_message("Activating Ruby LSP Rails add-on v#{VERSION}")
|
|
58
66
|
|
|
67
|
+
addon_settings = @global_state.settings_for_addon(name)
|
|
68
|
+
@settings.merge!(addon_settings) if addon_settings
|
|
69
|
+
|
|
59
70
|
register_additional_file_watchers(global_state: global_state, outgoing_queue: outgoing_queue)
|
|
60
71
|
|
|
61
72
|
# Start booting the real client in a background thread. Until this completes, the client will be a NullClient
|
|
@@ -81,7 +92,9 @@ module RubyLsp
|
|
|
81
92
|
).void
|
|
82
93
|
end
|
|
83
94
|
def create_code_lens_listener(response_builder, uri, dispatcher)
|
|
84
|
-
|
|
95
|
+
return unless @global_state
|
|
96
|
+
|
|
97
|
+
CodeLens.new(@rails_runner_client, @global_state, response_builder, uri, dispatcher)
|
|
85
98
|
end
|
|
86
99
|
|
|
87
100
|
sig do
|
|
@@ -92,7 +105,9 @@ module RubyLsp
|
|
|
92
105
|
).void
|
|
93
106
|
end
|
|
94
107
|
def create_hover_listener(response_builder, node_context, dispatcher)
|
|
95
|
-
|
|
108
|
+
return unless @global_state
|
|
109
|
+
|
|
110
|
+
Hover.new(@rails_runner_client, response_builder, node_context, @global_state, dispatcher)
|
|
96
111
|
end
|
|
97
112
|
|
|
98
113
|
sig do
|
|
@@ -116,8 +131,9 @@ module RubyLsp
|
|
|
116
131
|
).void
|
|
117
132
|
end
|
|
118
133
|
def create_definition_listener(response_builder, uri, node_context, dispatcher)
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
return unless @global_state
|
|
135
|
+
|
|
136
|
+
Definition.new(@rails_runner_client, response_builder, node_context, @global_state.index, dispatcher)
|
|
121
137
|
end
|
|
122
138
|
|
|
123
139
|
sig do
|
|
@@ -251,6 +267,7 @@ module RubyLsp
|
|
|
251
267
|
def offer_to_run_pending_migrations
|
|
252
268
|
return unless @outgoing_queue
|
|
253
269
|
return unless @global_state&.client_capabilities&.window_show_message_supports_extra_properties
|
|
270
|
+
return unless @settings[:enablePendingMigrationsPrompt]
|
|
254
271
|
|
|
255
272
|
migration_message = @rails_runner_client.pending_migrations_message
|
|
256
273
|
return unless migration_message
|
|
@@ -10,10 +10,10 @@ module RubyLsp
|
|
|
10
10
|
class << self
|
|
11
11
|
extend T::Sig
|
|
12
12
|
|
|
13
|
-
sig { params(outgoing_queue: Thread::Queue).returns(RunnerClient) }
|
|
14
|
-
def create_client(outgoing_queue)
|
|
13
|
+
sig { params(outgoing_queue: Thread::Queue, global_state: RubyLsp::GlobalState).returns(RunnerClient) }
|
|
14
|
+
def create_client(outgoing_queue, global_state)
|
|
15
15
|
if File.exist?("bin/rails")
|
|
16
|
-
new(outgoing_queue)
|
|
16
|
+
new(outgoing_queue, global_state)
|
|
17
17
|
else
|
|
18
18
|
unless outgoing_queue.closed?
|
|
19
19
|
outgoing_queue << RubyLsp::Notification.window_log_message(
|
|
@@ -51,8 +51,8 @@ module RubyLsp
|
|
|
51
51
|
sig { returns(String) }
|
|
52
52
|
attr_reader :rails_root
|
|
53
53
|
|
|
54
|
-
sig { params(outgoing_queue: Thread::Queue).void }
|
|
55
|
-
def initialize(outgoing_queue)
|
|
54
|
+
sig { params(outgoing_queue: Thread::Queue, global_state: RubyLsp::GlobalState).void }
|
|
55
|
+
def initialize(outgoing_queue, global_state)
|
|
56
56
|
@outgoing_queue = T.let(outgoing_queue, Thread::Queue)
|
|
57
57
|
@mutex = T.let(Mutex.new, Mutex)
|
|
58
58
|
# Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
|
|
@@ -71,7 +71,15 @@ module RubyLsp
|
|
|
71
71
|
log_message("Ruby LSP Rails booting server")
|
|
72
72
|
|
|
73
73
|
stdin, stdout, stderr, wait_thread = Bundler.with_original_env do
|
|
74
|
-
Open3.popen3(
|
|
74
|
+
Open3.popen3(
|
|
75
|
+
"bundle",
|
|
76
|
+
"exec",
|
|
77
|
+
"rails",
|
|
78
|
+
"runner",
|
|
79
|
+
"#{__dir__}/server.rb",
|
|
80
|
+
"start",
|
|
81
|
+
server_relevant_capabilities(global_state),
|
|
82
|
+
)
|
|
75
83
|
end
|
|
76
84
|
|
|
77
85
|
@stdin = T.let(stdin, IO)
|
|
@@ -100,10 +108,16 @@ module RubyLsp
|
|
|
100
108
|
end
|
|
101
109
|
end
|
|
102
110
|
|
|
103
|
-
|
|
111
|
+
# Responsible for transmitting notifications coming from the server to the outgoing queue, so that we can do
|
|
112
|
+
# things such as showing progress notifications initiated by the server
|
|
113
|
+
@notifier_thread = T.let(
|
|
104
114
|
Thread.new do
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
until @stderr.closed?
|
|
116
|
+
notification = read_notification
|
|
117
|
+
|
|
118
|
+
unless @outgoing_queue.closed? || !notification
|
|
119
|
+
@outgoing_queue << notification
|
|
120
|
+
end
|
|
107
121
|
end
|
|
108
122
|
rescue IOError
|
|
109
123
|
# The server was shutdown and stderr is already closed
|
|
@@ -259,6 +273,13 @@ module RubyLsp
|
|
|
259
273
|
[@stdin, @stdout, @stderr].all?(&:closed?) && !@wait_thread.alive?
|
|
260
274
|
end
|
|
261
275
|
|
|
276
|
+
sig { returns(T::Boolean) }
|
|
277
|
+
def connected?
|
|
278
|
+
true
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
private
|
|
282
|
+
|
|
262
283
|
sig do
|
|
263
284
|
params(
|
|
264
285
|
request: String,
|
|
@@ -274,8 +295,6 @@ module RubyLsp
|
|
|
274
295
|
sig { params(request: String, params: T.untyped).void }
|
|
275
296
|
def send_notification(request, **params) = send_message(request, **params)
|
|
276
297
|
|
|
277
|
-
private
|
|
278
|
-
|
|
279
298
|
sig { overridable.params(request: String, params: T.untyped).void }
|
|
280
299
|
def send_message(request, **params)
|
|
281
300
|
message = { method: request }
|
|
@@ -338,6 +357,28 @@ module RubyLsp
|
|
|
338
357
|
|
|
339
358
|
length.to_i
|
|
340
359
|
end
|
|
360
|
+
|
|
361
|
+
# Read a server notification from stderr. Only intended to be used by notifier thread
|
|
362
|
+
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
363
|
+
def read_notification
|
|
364
|
+
headers = @stderr.gets("\r\n\r\n")
|
|
365
|
+
return unless headers
|
|
366
|
+
|
|
367
|
+
length = headers[/Content-Length: (\d+)/i, 1]
|
|
368
|
+
return unless length
|
|
369
|
+
|
|
370
|
+
raw_content = @stderr.read(length.to_i)
|
|
371
|
+
return unless raw_content
|
|
372
|
+
|
|
373
|
+
JSON.parse(raw_content, symbolize_names: true)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
sig { params(global_state: GlobalState).returns(String) }
|
|
377
|
+
def server_relevant_capabilities(global_state)
|
|
378
|
+
{
|
|
379
|
+
supports_progress: global_state.client_capabilities.supports_progress,
|
|
380
|
+
}.to_json
|
|
381
|
+
end
|
|
341
382
|
end
|
|
342
383
|
|
|
343
384
|
class NullClient < RunnerClient
|
|
@@ -362,6 +403,11 @@ module RubyLsp
|
|
|
362
403
|
Dir.pwd
|
|
363
404
|
end
|
|
364
405
|
|
|
406
|
+
sig { returns(T::Boolean) }
|
|
407
|
+
def connected?
|
|
408
|
+
false
|
|
409
|
+
end
|
|
410
|
+
|
|
365
411
|
private
|
|
366
412
|
|
|
367
413
|
sig { params(message: ::String, type: ::Integer).void }
|
|
@@ -3,19 +3,42 @@
|
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "open3"
|
|
6
|
+
require "delegate"
|
|
6
7
|
|
|
7
8
|
module RubyLsp
|
|
8
9
|
module Rails
|
|
9
10
|
module Common
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
class Progress
|
|
12
|
+
def initialize(stderr, id, supports_progress)
|
|
13
|
+
@stderr = stderr
|
|
14
|
+
@id = id
|
|
15
|
+
@supports_progress = supports_progress
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def report(percentage: nil, message: nil)
|
|
19
|
+
return unless @supports_progress
|
|
20
|
+
return unless percentage || message
|
|
21
|
+
|
|
22
|
+
json_message = {
|
|
23
|
+
method: "$/progress",
|
|
24
|
+
params: {
|
|
25
|
+
token: @id,
|
|
26
|
+
value: {
|
|
27
|
+
kind: "report",
|
|
28
|
+
percentage: percentage,
|
|
29
|
+
message: message,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}.to_json
|
|
33
|
+
|
|
34
|
+
@stderr.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
|
|
35
|
+
end
|
|
14
36
|
end
|
|
15
37
|
|
|
16
|
-
# Log a message to the editor's output panel
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
# Log a message to the editor's output panel. The type is the number of the message type, which can be found in
|
|
39
|
+
# the specification https://microsoft.github.io/language-server-protocol/specification/#messageType
|
|
40
|
+
def log_message(message, type: 4)
|
|
41
|
+
send_notification({ method: "window/logMessage", params: { type: type, message: message } })
|
|
19
42
|
end
|
|
20
43
|
|
|
21
44
|
# Sends an error result to a request, if the request failed. DO NOT INVOKE THIS METHOD FOR NOTIFICATIONS! Use
|
|
@@ -54,6 +77,82 @@ module RubyLsp
|
|
|
54
77
|
rescue => e
|
|
55
78
|
log_message("Request #{notification_name} failed:\n#{e.full_message(highlight: false)}")
|
|
56
79
|
end
|
|
80
|
+
|
|
81
|
+
def begin_progress(id, title, percentage: nil, message: nil)
|
|
82
|
+
return unless @capabilities[:supports_progress]
|
|
83
|
+
|
|
84
|
+
# This is actually a request, but it is sent asynchronously and we do not return the response back to the
|
|
85
|
+
# server, so we consider it a notification from the perspective of the client/runtime server dynamic
|
|
86
|
+
send_notification({
|
|
87
|
+
id: "progress-request-#{id}",
|
|
88
|
+
method: "window/workDoneProgress/create",
|
|
89
|
+
params: { token: id },
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
send_notification({
|
|
93
|
+
method: "$/progress",
|
|
94
|
+
params: {
|
|
95
|
+
token: id,
|
|
96
|
+
value: {
|
|
97
|
+
kind: "begin",
|
|
98
|
+
title: title,
|
|
99
|
+
percentage: percentage,
|
|
100
|
+
message: message,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def report_progress(id, percentage: nil, message: nil)
|
|
107
|
+
return unless @capabilities[:supports_progress]
|
|
108
|
+
|
|
109
|
+
send_notification({
|
|
110
|
+
method: "$/progress",
|
|
111
|
+
params: {
|
|
112
|
+
token: id,
|
|
113
|
+
value: {
|
|
114
|
+
kind: "report",
|
|
115
|
+
percentage: percentage,
|
|
116
|
+
message: message,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def end_progress(id)
|
|
123
|
+
return unless @capabilities[:supports_progress]
|
|
124
|
+
|
|
125
|
+
send_notification({
|
|
126
|
+
method: "$/progress",
|
|
127
|
+
params: {
|
|
128
|
+
token: id,
|
|
129
|
+
value: { kind: "end" },
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def with_progress(id, title, percentage: nil, message: nil, &block)
|
|
135
|
+
progress_block = Progress.new(@stderr, id, @capabilities[:supports_progress])
|
|
136
|
+
return block.call(progress_block) unless @capabilities[:supports_progress]
|
|
137
|
+
|
|
138
|
+
begin_progress(id, title, percentage: percentage, message: message)
|
|
139
|
+
block.call(progress_block)
|
|
140
|
+
end_progress(id)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# Write a response message back to the client
|
|
146
|
+
def send_message(message)
|
|
147
|
+
json_message = message.to_json
|
|
148
|
+
@stdout.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Write a notification to the client to be transmitted to the editor
|
|
152
|
+
def send_notification(message)
|
|
153
|
+
json_message = message.to_json
|
|
154
|
+
@stderr.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
|
|
155
|
+
end
|
|
57
156
|
end
|
|
58
157
|
|
|
59
158
|
class ServerAddon
|
|
@@ -76,16 +175,18 @@ module RubyLsp
|
|
|
76
175
|
end
|
|
77
176
|
|
|
78
177
|
# Instantiate all server addons and store them in a hash for easy access after we have discovered the classes
|
|
79
|
-
def finalize_registrations!(stdout)
|
|
178
|
+
def finalize_registrations!(stdout, stderr, capabilities)
|
|
80
179
|
until @server_addon_classes.empty?
|
|
81
|
-
addon = @server_addon_classes.shift.new(stdout)
|
|
180
|
+
addon = @server_addon_classes.shift.new(stdout, stderr, capabilities)
|
|
82
181
|
@server_addons[addon.name] = addon
|
|
83
182
|
end
|
|
84
183
|
end
|
|
85
184
|
end
|
|
86
185
|
|
|
87
|
-
def initialize(stdout)
|
|
186
|
+
def initialize(stdout, stderr, capabilities)
|
|
88
187
|
@stdout = stdout
|
|
188
|
+
@stderr = stderr
|
|
189
|
+
@capabilities = capabilities
|
|
89
190
|
end
|
|
90
191
|
|
|
91
192
|
def name
|
|
@@ -97,14 +198,31 @@ module RubyLsp
|
|
|
97
198
|
end
|
|
98
199
|
end
|
|
99
200
|
|
|
201
|
+
class IOWrapper < SimpleDelegator
|
|
202
|
+
def puts(*args)
|
|
203
|
+
args.each { |arg| log("#{arg}\n") }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def print(*args)
|
|
207
|
+
args.each { |arg| log(arg.to_s) }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
private
|
|
211
|
+
|
|
212
|
+
def log(message)
|
|
213
|
+
json_message = { method: "window/logMessage", params: { type: 4, message: message } }.to_json
|
|
214
|
+
write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
100
218
|
class Server
|
|
101
219
|
include Common
|
|
102
220
|
|
|
103
|
-
def initialize(stdout: $stdout, override_default_output_device: true)
|
|
221
|
+
def initialize(stdout: $stdout, stderr: $stderr, override_default_output_device: true, capabilities: {})
|
|
104
222
|
# Grab references to the original pipes so that we can change the default output device further down
|
|
105
223
|
@stdin = $stdin
|
|
106
224
|
@stdout = stdout
|
|
107
|
-
@stderr =
|
|
225
|
+
@stderr = stderr
|
|
108
226
|
@stdin.sync = true
|
|
109
227
|
@stdout.sync = true
|
|
110
228
|
@stderr.sync = true
|
|
@@ -112,10 +230,13 @@ module RubyLsp
|
|
|
112
230
|
@stdout.binmode
|
|
113
231
|
@stderr.binmode
|
|
114
232
|
|
|
233
|
+
# A hash containing the capabilities of the editor that may be relevant for the runtime server
|
|
234
|
+
@capabilities = capabilities
|
|
235
|
+
|
|
115
236
|
# # Set the default output device to be $stderr. This means that using `puts` by itself will default to printing
|
|
116
237
|
# # to $stderr and only explicit `$stdout.puts` will go to $stdout. This reduces the chance that output coming
|
|
117
238
|
# # from the Rails app will be accidentally sent to the client
|
|
118
|
-
$> =
|
|
239
|
+
$> = IOWrapper.new(@stderr) if override_default_output_device
|
|
119
240
|
|
|
120
241
|
@running = true
|
|
121
242
|
end
|
|
@@ -155,8 +276,10 @@ module RubyLsp
|
|
|
155
276
|
send_result(run_migrations)
|
|
156
277
|
end
|
|
157
278
|
when "reload"
|
|
158
|
-
|
|
159
|
-
|
|
279
|
+
with_progress("rails-reload", "Reloading Ruby LSP Rails instance") do
|
|
280
|
+
with_notification_error_handling(request) do
|
|
281
|
+
::Rails.application.reloader.reload!
|
|
282
|
+
end
|
|
160
283
|
end
|
|
161
284
|
when "route_location"
|
|
162
285
|
with_request_error_handling(request) do
|
|
@@ -169,7 +292,7 @@ module RubyLsp
|
|
|
169
292
|
when "server_addon/register"
|
|
170
293
|
with_notification_error_handling(request) do
|
|
171
294
|
require params[:server_addon_path]
|
|
172
|
-
ServerAddon.finalize_registrations!(@stdout)
|
|
295
|
+
ServerAddon.finalize_registrations!(@stdout, @stderr, @capabilities)
|
|
173
296
|
end
|
|
174
297
|
when "server_addon/delegate"
|
|
175
298
|
server_addon_name = params[:server_addon_name]
|
|
@@ -316,4 +439,6 @@ module RubyLsp
|
|
|
316
439
|
end
|
|
317
440
|
end
|
|
318
441
|
|
|
319
|
-
|
|
442
|
+
if ARGV.first == "start"
|
|
443
|
+
RubyLsp::Rails::Server.new(capabilities: JSON.parse(ARGV[1], symbolize_names: true)).start
|
|
444
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-lsp-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shopify
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-
|
|
10
|
+
date: 2025-02-04 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ruby-lsp
|
|
@@ -78,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
78
78
|
- !ruby/object:Gem::Version
|
|
79
79
|
version: '0'
|
|
80
80
|
requirements: []
|
|
81
|
-
rubygems_version: 3.6.
|
|
81
|
+
rubygems_version: 3.6.3
|
|
82
82
|
specification_version: 4
|
|
83
83
|
summary: A Ruby LSP addon for Rails
|
|
84
84
|
test_files: []
|