ruby-lsp-rails 0.3.31 → 0.4.8
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/Rakefile +1 -1
- data/lib/ruby_lsp/ruby_lsp_rails/addon.rb +74 -66
- data/lib/ruby_lsp/ruby_lsp_rails/code_lens.rb +39 -38
- data/lib/ruby_lsp/ruby_lsp_rails/completion.rb +7 -15
- data/lib/ruby_lsp/ruby_lsp_rails/definition.rb +14 -23
- data/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +24 -30
- data/lib/ruby_lsp/ruby_lsp_rails/hover.rb +106 -32
- data/lib/ruby_lsp/ruby_lsp_rails/indexing_enhancement.rb +7 -20
- data/lib/ruby_lsp/ruby_lsp_rails/rails_test_style.rb +171 -0
- data/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb +109 -78
- data/lib/ruby_lsp/ruby_lsp_rails/server.rb +254 -39
- data/lib/ruby_lsp/ruby_lsp_rails/support/active_support_test_case_helper.rb +3 -4
- data/lib/ruby_lsp/ruby_lsp_rails/support/associations.rb +6 -9
- data/lib/ruby_lsp/ruby_lsp_rails/support/callbacks.rb +54 -57
- data/lib/ruby_lsp/ruby_lsp_rails/support/location_builder.rb +1 -3
- data/lib/ruby_lsp_rails/version.rb +1 -1
- metadata +8 -7
|
@@ -1,36 +1,66 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "open3"
|
|
6
|
+
require "delegate"
|
|
6
7
|
|
|
7
8
|
module RubyLsp
|
|
8
9
|
module Rails
|
|
10
|
+
# @requires_ancestor: ServerComponent
|
|
9
11
|
module Common
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
class Progress
|
|
13
|
+
#: (IO | StringIO, String, bool) -> void
|
|
14
|
+
def initialize(stderr, id, supports_progress)
|
|
15
|
+
@stderr = stderr
|
|
16
|
+
@id = id
|
|
17
|
+
@supports_progress = supports_progress
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#: (?percentage: Integer?, ?message: String?) -> void
|
|
21
|
+
def report(percentage: nil, message: nil)
|
|
22
|
+
return unless @supports_progress
|
|
23
|
+
return unless percentage || message
|
|
24
|
+
|
|
25
|
+
json_message = {
|
|
26
|
+
method: "$/progress",
|
|
27
|
+
params: {
|
|
28
|
+
token: @id,
|
|
29
|
+
value: {
|
|
30
|
+
kind: "report",
|
|
31
|
+
percentage: percentage,
|
|
32
|
+
message: message,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}.to_json
|
|
36
|
+
|
|
37
|
+
@stderr.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
|
|
38
|
+
end
|
|
14
39
|
end
|
|
15
40
|
|
|
16
|
-
# Log a message to the editor's output panel
|
|
17
|
-
|
|
18
|
-
|
|
41
|
+
# Log a message to the editor's output panel. The type is the number of the message type, which can be found in
|
|
42
|
+
# the specification https://microsoft.github.io/language-server-protocol/specification/#messageType
|
|
43
|
+
#: (String, ?type: Integer) -> void
|
|
44
|
+
def log_message(message, type: 4)
|
|
45
|
+
send_notification({ method: "window/logMessage", params: { type: type, message: message } })
|
|
19
46
|
end
|
|
20
47
|
|
|
21
48
|
# Sends an error result to a request, if the request failed. DO NOT INVOKE THIS METHOD FOR NOTIFICATIONS! Use
|
|
22
49
|
# `log_message` instead, otherwise the client/server communication will go out of sync
|
|
50
|
+
#: (String) -> void
|
|
23
51
|
def send_error_response(message)
|
|
24
52
|
send_message({ error: message })
|
|
25
53
|
end
|
|
26
54
|
|
|
27
55
|
# Sends a result back to the client
|
|
56
|
+
#: (Hash[Symbol | String, untyped]?) -> void
|
|
28
57
|
def send_result(result)
|
|
29
58
|
send_message({ result: result })
|
|
30
59
|
end
|
|
31
60
|
|
|
32
61
|
# Handle possible errors for a request. This should only be used for requests, which means messages that return a
|
|
33
62
|
# response back to the client. Errors are returned as an error object back to the client
|
|
63
|
+
#: (String) { () -> void } -> void
|
|
34
64
|
def with_request_error_handling(request_name, &block)
|
|
35
65
|
block.call
|
|
36
66
|
rescue ActiveRecord::ConnectionNotEstablished
|
|
@@ -38,12 +68,15 @@ module RubyLsp
|
|
|
38
68
|
send_error_response("Request #{request_name} failed because database connection was not established.")
|
|
39
69
|
rescue ActiveRecord::NoDatabaseError
|
|
40
70
|
send_error_response("Request #{request_name} failed because the database does not exist.")
|
|
41
|
-
rescue => e
|
|
42
|
-
send_error_response("Request #{request_name} failed:\n#{e.full_message(highlight: false)}")
|
|
71
|
+
rescue NotImplementedError, LoadError, SyntaxError, SystemExit, SystemStackError => e
|
|
72
|
+
send_error_response("Request #{request_name} failed with #{e.class}:\n#{e.full_message(highlight: false)}")
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
send_error_response("Request #{request_name} failed with StandardError:\n#{e.full_message(highlight: false)}")
|
|
43
75
|
end
|
|
44
76
|
|
|
45
77
|
# Handle possible errors for a notification. This should only be used for notifications, which means messages that
|
|
46
78
|
# do not return a response back to the client. Errors are logged to the editor's output panel
|
|
79
|
+
#: (String) { () -> void } -> void
|
|
47
80
|
def with_notification_error_handling(notification_name, &block)
|
|
48
81
|
block.call
|
|
49
82
|
rescue ActiveRecord::ConnectionNotEstablished
|
|
@@ -51,60 +84,188 @@ module RubyLsp
|
|
|
51
84
|
log_message("Request #{notification_name} failed because database connection was not established.")
|
|
52
85
|
rescue ActiveRecord::NoDatabaseError
|
|
53
86
|
log_message("Request #{notification_name} failed because the database does not exist.")
|
|
54
|
-
rescue => e
|
|
55
|
-
log_message("Request #{notification_name} failed:\n#{e.full_message(highlight: false)}")
|
|
87
|
+
rescue NotImplementedError, LoadError, SyntaxError, SystemExit, SystemStackError => e
|
|
88
|
+
log_message("Request #{notification_name} failed with #{e.class}:\n#{e.full_message(highlight: false)}")
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
log_message("Request #{notification_name} failed with StandardError:\n#{e.full_message(highlight: false)}")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#: (String, String, ?percentage: Integer?, ?message: String?) -> void
|
|
94
|
+
def begin_progress(id, title, percentage: nil, message: nil)
|
|
95
|
+
return unless capabilities[:supports_progress]
|
|
96
|
+
|
|
97
|
+
# This is actually a request, but it is sent asynchronously and we do not return the response back to the
|
|
98
|
+
# server, so we consider it a notification from the perspective of the client/runtime server dynamic
|
|
99
|
+
send_notification({
|
|
100
|
+
id: "progress-request-#{id}",
|
|
101
|
+
method: "window/workDoneProgress/create",
|
|
102
|
+
params: { token: id },
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
send_notification({
|
|
106
|
+
method: "$/progress",
|
|
107
|
+
params: {
|
|
108
|
+
token: id,
|
|
109
|
+
value: {
|
|
110
|
+
kind: "begin",
|
|
111
|
+
title: title,
|
|
112
|
+
percentage: percentage,
|
|
113
|
+
message: message,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
#: (String, ?percentage: Integer?, ?message: String?) -> void
|
|
120
|
+
def report_progress(id, percentage: nil, message: nil)
|
|
121
|
+
return unless capabilities[:supports_progress]
|
|
122
|
+
|
|
123
|
+
send_notification({
|
|
124
|
+
method: "$/progress",
|
|
125
|
+
params: {
|
|
126
|
+
token: id,
|
|
127
|
+
value: {
|
|
128
|
+
kind: "report",
|
|
129
|
+
percentage: percentage,
|
|
130
|
+
message: message,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
#: (String) -> void
|
|
137
|
+
def end_progress(id)
|
|
138
|
+
return unless capabilities[:supports_progress]
|
|
139
|
+
|
|
140
|
+
send_notification({
|
|
141
|
+
method: "$/progress",
|
|
142
|
+
params: {
|
|
143
|
+
token: id,
|
|
144
|
+
value: { kind: "end" },
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
#: (String, String, ?percentage: Integer?, ?message: String?) { (Progress) -> void } -> void
|
|
150
|
+
def with_progress(id, title, percentage: nil, message: nil, &block)
|
|
151
|
+
progress_block = Progress.new(stderr, id, capabilities[:supports_progress])
|
|
152
|
+
return block.call(progress_block) unless capabilities[:supports_progress]
|
|
153
|
+
|
|
154
|
+
begin_progress(id, title, percentage: percentage, message: message)
|
|
155
|
+
block.call(progress_block)
|
|
156
|
+
end_progress(id)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
# Write a response message back to the client
|
|
162
|
+
#: (Hash[String | Symbol, untyped]) -> void
|
|
163
|
+
def send_message(message)
|
|
164
|
+
json_message = message.to_json
|
|
165
|
+
stdout.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Write a notification to the client to be transmitted to the editor
|
|
169
|
+
#: (Hash[String | Symbol, untyped]) -> void
|
|
170
|
+
def send_notification(message)
|
|
171
|
+
json_message = message.to_json
|
|
172
|
+
stderr.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class ServerComponent
|
|
177
|
+
#: IO | StringIO
|
|
178
|
+
attr_reader :stdout
|
|
179
|
+
|
|
180
|
+
#: IO | StringIO
|
|
181
|
+
attr_reader :stderr
|
|
182
|
+
|
|
183
|
+
#: Hash[Symbol | String, untyped]
|
|
184
|
+
attr_reader :capabilities
|
|
185
|
+
|
|
186
|
+
#: (IO | StringIO, IO | StringIO, Hash[Symbol | String, untyped]) -> void
|
|
187
|
+
def initialize(stdout, stderr, capabilities)
|
|
188
|
+
@stdout = stdout
|
|
189
|
+
@stderr = stderr
|
|
190
|
+
@capabilities = capabilities
|
|
56
191
|
end
|
|
57
192
|
end
|
|
58
193
|
|
|
59
|
-
class ServerAddon
|
|
194
|
+
class ServerAddon < ServerComponent
|
|
60
195
|
include Common
|
|
61
196
|
|
|
62
|
-
@server_addon_classes = []
|
|
63
|
-
@server_addons = {}
|
|
197
|
+
@server_addon_classes = [] #: Array[singleton(ServerAddon)]
|
|
198
|
+
@server_addons = {} #: Hash[String, ServerAddon]
|
|
64
199
|
|
|
65
200
|
class << self
|
|
66
201
|
# We keep track of runtime server add-ons the same way we track other add-ons, by storing classes that inherit
|
|
67
202
|
# from the base one
|
|
203
|
+
#: (singleton(ServerAddon)) -> void
|
|
68
204
|
def inherited(child)
|
|
69
205
|
@server_addon_classes << child
|
|
70
206
|
super
|
|
71
207
|
end
|
|
72
208
|
|
|
73
209
|
# Delegate `request` with `params` to the server add-on with the given `name`
|
|
210
|
+
#: (String, String, Hash[Symbol | String, untyped]) -> void
|
|
74
211
|
def delegate(name, request, params)
|
|
75
212
|
@server_addons[name]&.execute(request, params)
|
|
76
213
|
end
|
|
77
214
|
|
|
78
215
|
# Instantiate all server addons and store them in a hash for easy access after we have discovered the classes
|
|
79
|
-
|
|
216
|
+
#: (IO | StringIO, IO | StringIO, Hash[Symbol | String, untyped]) -> void
|
|
217
|
+
def finalize_registrations!(stdout, stderr, capabilities)
|
|
80
218
|
until @server_addon_classes.empty?
|
|
81
|
-
addon = @server_addon_classes.shift
|
|
219
|
+
addon = @server_addon_classes.shift #: as !nil
|
|
220
|
+
.new(stdout, stderr, capabilities)
|
|
82
221
|
@server_addons[addon.name] = addon
|
|
83
222
|
end
|
|
84
223
|
end
|
|
85
224
|
end
|
|
86
225
|
|
|
87
|
-
|
|
88
|
-
@stdout = stdout
|
|
89
|
-
end
|
|
90
|
-
|
|
226
|
+
#: -> String
|
|
91
227
|
def name
|
|
92
228
|
raise NotImplementedError, "Not implemented!"
|
|
93
229
|
end
|
|
94
230
|
|
|
231
|
+
#: (String, Hash[String | Symbol, untyped]) -> untyped
|
|
95
232
|
def execute(request, params)
|
|
96
233
|
raise NotImplementedError, "Not implemented!"
|
|
97
234
|
end
|
|
98
235
|
end
|
|
99
236
|
|
|
100
|
-
class
|
|
237
|
+
class IOWrapper < SimpleDelegator
|
|
238
|
+
#: (*untyped) -> void
|
|
239
|
+
def puts(*args)
|
|
240
|
+
args.each { |arg| log("#{arg}\n") }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
#: (*untyped) -> void
|
|
244
|
+
def print(*args)
|
|
245
|
+
args.each { |arg| log(arg.to_s) }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
private
|
|
249
|
+
|
|
250
|
+
#: (untyped) -> void
|
|
251
|
+
def log(message)
|
|
252
|
+
json_message = { method: "window/logMessage", params: { type: 4, message: message } }.to_json
|
|
253
|
+
|
|
254
|
+
self #: as untyped # rubocop:disable Style/RedundantSelf
|
|
255
|
+
.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
class Server < ServerComponent
|
|
101
260
|
include Common
|
|
102
261
|
|
|
103
|
-
|
|
262
|
+
#: (?stdout: IO | StringIO, ?stderr: IO | StringIO, ?override_default_output_device: bool, ?capabilities: Hash[Symbol | String, untyped]) -> void
|
|
263
|
+
def initialize(stdout: $stdout, stderr: $stderr, override_default_output_device: true, capabilities: {})
|
|
104
264
|
# Grab references to the original pipes so that we can change the default output device further down
|
|
105
|
-
|
|
106
|
-
@
|
|
107
|
-
|
|
265
|
+
|
|
266
|
+
@stdin = $stdin #: IO
|
|
267
|
+
super(stdout, stderr, capabilities)
|
|
268
|
+
|
|
108
269
|
@stdin.sync = true
|
|
109
270
|
@stdout.sync = true
|
|
110
271
|
@stderr.sync = true
|
|
@@ -115,25 +276,28 @@ module RubyLsp
|
|
|
115
276
|
# # Set the default output device to be $stderr. This means that using `puts` by itself will default to printing
|
|
116
277
|
# # to $stderr and only explicit `$stdout.puts` will go to $stdout. This reduces the chance that output coming
|
|
117
278
|
# # from the Rails app will be accidentally sent to the client
|
|
118
|
-
$> =
|
|
279
|
+
$> = IOWrapper.new(@stderr) if override_default_output_device
|
|
119
280
|
|
|
120
|
-
@running = true
|
|
281
|
+
@running = true #: bool
|
|
282
|
+
@database_supports_indexing = nil #: bool?
|
|
121
283
|
end
|
|
122
284
|
|
|
285
|
+
#: -> void
|
|
123
286
|
def start
|
|
124
287
|
load_routes
|
|
125
288
|
clear_file_system_resolver_hooks
|
|
126
289
|
send_result({ message: "ok", root: ::Rails.root.to_s })
|
|
127
290
|
|
|
128
291
|
while @running
|
|
129
|
-
headers = @stdin.gets("\r\n\r\n")
|
|
130
|
-
json = @stdin.read(headers[/Content-Length: (\d+)/i, 1].to_i)
|
|
292
|
+
headers = @stdin.gets("\r\n\r\n") #: as String
|
|
293
|
+
json = @stdin.read(headers[/Content-Length: (\d+)/i, 1].to_i) #: as String
|
|
131
294
|
|
|
132
295
|
request = JSON.parse(json, symbolize_names: true)
|
|
133
296
|
execute(request.fetch(:method), request[:params])
|
|
134
297
|
end
|
|
135
298
|
end
|
|
136
299
|
|
|
300
|
+
#: (String, Hash[Symbol | String, untyped]) -> void
|
|
137
301
|
def execute(request, params)
|
|
138
302
|
case request
|
|
139
303
|
when "shutdown"
|
|
@@ -142,7 +306,7 @@ module RubyLsp
|
|
|
142
306
|
with_request_error_handling(request) do
|
|
143
307
|
send_result(resolve_database_info_from_model(params.fetch(:name)))
|
|
144
308
|
end
|
|
145
|
-
when "
|
|
309
|
+
when "association_target"
|
|
146
310
|
with_request_error_handling(request) do
|
|
147
311
|
send_result(resolve_association_target(params))
|
|
148
312
|
end
|
|
@@ -155,8 +319,10 @@ module RubyLsp
|
|
|
155
319
|
send_result(run_migrations)
|
|
156
320
|
end
|
|
157
321
|
when "reload"
|
|
158
|
-
|
|
159
|
-
|
|
322
|
+
with_progress("rails-reload", "Reloading Ruby LSP Rails instance") do
|
|
323
|
+
with_notification_error_handling(request) do
|
|
324
|
+
::Rails.application.reloader.reload!
|
|
325
|
+
end
|
|
160
326
|
end
|
|
161
327
|
when "route_location"
|
|
162
328
|
with_request_error_handling(request) do
|
|
@@ -169,7 +335,7 @@ module RubyLsp
|
|
|
169
335
|
when "server_addon/register"
|
|
170
336
|
with_notification_error_handling(request) do
|
|
171
337
|
require params[:server_addon_path]
|
|
172
|
-
ServerAddon.finalize_registrations!(@stdout)
|
|
338
|
+
ServerAddon.finalize_registrations!(@stdout, @stderr, @capabilities)
|
|
173
339
|
end
|
|
174
340
|
when "server_addon/delegate"
|
|
175
341
|
server_addon_name = params[:server_addon_name]
|
|
@@ -183,6 +349,7 @@ module RubyLsp
|
|
|
183
349
|
|
|
184
350
|
private
|
|
185
351
|
|
|
352
|
+
#: (Hash[Symbol | String, untyped]) -> Hash[Symbol | String, untyped]?
|
|
186
353
|
def resolve_route_info(requirements)
|
|
187
354
|
if requirements[:controller]
|
|
188
355
|
requirements[:controller] = requirements.fetch(:controller).underscore.delete_suffix("_controller")
|
|
@@ -211,6 +378,7 @@ module RubyLsp
|
|
|
211
378
|
# We also check that it's enabled.
|
|
212
379
|
if ActionDispatch::Routing::Mapper.respond_to?(:route_source_locations) &&
|
|
213
380
|
ActionDispatch::Routing::Mapper.route_source_locations
|
|
381
|
+
#: (String) -> Hash[Symbol | String, untyped]?
|
|
214
382
|
def route_location(name)
|
|
215
383
|
# In Rails 8, Rails.application.routes.named_routes is not populated by default
|
|
216
384
|
if ::Rails.application.respond_to?(:reload_routes_unless_loaded)
|
|
@@ -229,18 +397,22 @@ module RubyLsp
|
|
|
229
397
|
{ location: ::Rails.root.join(route.source_location).to_s }
|
|
230
398
|
end
|
|
231
399
|
else
|
|
400
|
+
#: (String) -> Hash[Symbol | String, untyped]?
|
|
232
401
|
def route_location(name)
|
|
233
402
|
nil
|
|
234
403
|
end
|
|
235
404
|
end
|
|
236
405
|
|
|
406
|
+
#: (String) -> Hash[Symbol | String, untyped]?
|
|
237
407
|
def resolve_database_info_from_model(model_name)
|
|
238
|
-
const = ActiveSupport::Inflector.safe_constantize(model_name)
|
|
408
|
+
const = ActiveSupport::Inflector.safe_constantize(model_name) # rubocop:disable Sorbet/ConstantsFromStrings
|
|
239
409
|
return unless active_record_model?(const)
|
|
240
410
|
|
|
241
411
|
info = {
|
|
242
412
|
columns: const.columns.map { |column| [column.name, column.type, column.default, column.null] },
|
|
243
413
|
primary_keys: Array(const.primary_key),
|
|
414
|
+
foreign_keys: collect_model_foreign_keys(const),
|
|
415
|
+
indexes: collect_model_indexes(const),
|
|
244
416
|
}
|
|
245
417
|
|
|
246
418
|
if ActiveRecord::Tasks::DatabaseTasks.respond_to?(:schema_dump_path)
|
|
@@ -250,28 +422,33 @@ module RubyLsp
|
|
|
250
422
|
info
|
|
251
423
|
end
|
|
252
424
|
|
|
425
|
+
#: (Hash[Symbol | String, untyped]) -> Hash[Symbol | String, untyped]?
|
|
253
426
|
def resolve_association_target(params)
|
|
254
|
-
const = ActiveSupport::Inflector.safe_constantize(params[:model_name])
|
|
427
|
+
const = ActiveSupport::Inflector.safe_constantize(params[:model_name]) # rubocop:disable Sorbet/ConstantsFromStrings
|
|
255
428
|
return unless active_record_model?(const)
|
|
256
429
|
|
|
257
430
|
association_klass = const.reflect_on_association(params[:association_name].intern).klass
|
|
258
431
|
source_location = Object.const_source_location(association_klass.to_s)
|
|
432
|
+
return unless source_location
|
|
259
433
|
|
|
260
|
-
{ location: source_location
|
|
434
|
+
{ location: "#{source_location[0]}:#{source_location[1]}", name: association_klass.name }
|
|
261
435
|
rescue NameError
|
|
262
436
|
nil
|
|
263
437
|
end
|
|
264
438
|
|
|
439
|
+
#: (Module?) -> bool
|
|
265
440
|
def active_record_model?(const)
|
|
266
441
|
!!(
|
|
267
442
|
const &&
|
|
268
443
|
defined?(ActiveRecord) &&
|
|
269
444
|
const.is_a?(Class) &&
|
|
270
445
|
ActiveRecord::Base > const && # We do this 'backwards' in case the class overwrites `<`
|
|
271
|
-
!const
|
|
446
|
+
!const #: as singleton(ActiveRecord::Base)
|
|
447
|
+
.abstract_class?
|
|
272
448
|
)
|
|
273
449
|
end
|
|
274
450
|
|
|
451
|
+
#: -> String?
|
|
275
452
|
def pending_migrations_message
|
|
276
453
|
# `check_all_pending!` is only available since Rails 7.1
|
|
277
454
|
return unless defined?(ActiveRecord) && ActiveRecord::Migration.respond_to?(:check_all_pending!)
|
|
@@ -282,6 +459,7 @@ module RubyLsp
|
|
|
282
459
|
e.message
|
|
283
460
|
end
|
|
284
461
|
|
|
462
|
+
#: -> Hash[Symbol | String, untyped]
|
|
285
463
|
def run_migrations
|
|
286
464
|
# Running migrations invokes `load` which will repeatedly load the same files. It's not designed to be invoked
|
|
287
465
|
# multiple times within the same process. To avoid any memory bloat, we run migrations in a separate process
|
|
@@ -293,6 +471,7 @@ module RubyLsp
|
|
|
293
471
|
{ message: stdout, status: status.exitstatus }
|
|
294
472
|
end
|
|
295
473
|
|
|
474
|
+
#: -> void
|
|
296
475
|
def load_routes
|
|
297
476
|
with_notification_error_handling("initial_load_routes") do
|
|
298
477
|
# Load routes if they haven't been loaded yet (see https://github.com/rails/rails/pull/51614).
|
|
@@ -305,6 +484,7 @@ module RubyLsp
|
|
|
305
484
|
# watches files. Since the Rails application is already booted by the time we reach this script, we can't no-op
|
|
306
485
|
# the file watcher implementation. Instead, we clear the hooks to prevent the registered file watchers from being
|
|
307
486
|
# instantiated
|
|
487
|
+
#: -> void
|
|
308
488
|
def clear_file_system_resolver_hooks
|
|
309
489
|
return unless defined?(::ActionView::PathRegistry)
|
|
310
490
|
|
|
@@ -312,8 +492,43 @@ module RubyLsp
|
|
|
312
492
|
::ActionView::PathRegistry.file_system_resolver_hooks.clear
|
|
313
493
|
end
|
|
314
494
|
end
|
|
495
|
+
|
|
496
|
+
#: (singleton(ActiveRecord::Base)) -> Array[String]
|
|
497
|
+
def collect_model_foreign_keys(model)
|
|
498
|
+
return [] unless model.connection.respond_to?(:supports_foreign_keys?) &&
|
|
499
|
+
model.connection.supports_foreign_keys?
|
|
500
|
+
|
|
501
|
+
model.connection.foreign_keys(model.table_name).map do |key_definition|
|
|
502
|
+
key_definition.options[:column]
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
#: (singleton(ActiveRecord::Base)) -> Array[Hash[Symbol, untyped]]
|
|
507
|
+
def collect_model_indexes(model)
|
|
508
|
+
return [] unless database_supports_indexing?(model)
|
|
509
|
+
|
|
510
|
+
model.connection.indexes(model.table_name).map do |index_definition|
|
|
511
|
+
{
|
|
512
|
+
name: index_definition.name,
|
|
513
|
+
columns: index_definition.columns,
|
|
514
|
+
unique: index_definition.unique,
|
|
515
|
+
}
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
#: (singleton(ActiveRecord::Base)) -> bool
|
|
520
|
+
def database_supports_indexing?(model)
|
|
521
|
+
return @database_supports_indexing unless @database_supports_indexing.nil?
|
|
522
|
+
|
|
523
|
+
model.connection.indexes(model.table_name)
|
|
524
|
+
@database_supports_indexing = true
|
|
525
|
+
rescue NotImplementedError
|
|
526
|
+
@database_supports_indexing = false
|
|
527
|
+
end
|
|
315
528
|
end
|
|
316
529
|
end
|
|
317
530
|
end
|
|
318
531
|
|
|
319
|
-
|
|
532
|
+
if ARGV.first == "start"
|
|
533
|
+
RubyLsp::Rails::Server.new(capabilities: JSON.parse(ARGV[1], symbolize_names: true)).start
|
|
534
|
+
end
|
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
module RubyLsp
|
|
5
5
|
module Rails
|
|
6
6
|
module ActiveSupportTestCaseHelper
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
sig { params(node: Prism::CallNode).returns(T.nilable(String)) }
|
|
7
|
+
#: (Prism::CallNode node) -> String?
|
|
10
8
|
def extract_test_case_name(node)
|
|
11
9
|
message_value = node.message
|
|
12
10
|
return unless message_value == "test" || message_value == "it"
|
|
@@ -21,7 +19,8 @@ module RubyLsp
|
|
|
21
19
|
parts = first_argument.parts
|
|
22
20
|
|
|
23
21
|
if parts.all? { |part| part.is_a?(Prism::StringNode) }
|
|
24
|
-
|
|
22
|
+
parts #: as Array[Prism::StringNode]
|
|
23
|
+
.map(&:content).join
|
|
25
24
|
end
|
|
26
25
|
when Prism::StringNode
|
|
27
26
|
first_argument.content
|
|
@@ -5,15 +5,12 @@ module RubyLsp
|
|
|
5
5
|
module Rails
|
|
6
6
|
module Support
|
|
7
7
|
module Associations
|
|
8
|
-
ALL =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
].freeze,
|
|
15
|
-
T::Array[String],
|
|
16
|
-
)
|
|
8
|
+
ALL = [
|
|
9
|
+
"belongs_to",
|
|
10
|
+
"has_many",
|
|
11
|
+
"has_one",
|
|
12
|
+
"has_and_belongs_to_many",
|
|
13
|
+
].freeze
|
|
17
14
|
end
|
|
18
15
|
end
|
|
19
16
|
end
|
|
@@ -5,66 +5,63 @@ module RubyLsp
|
|
|
5
5
|
module Rails
|
|
6
6
|
module Support
|
|
7
7
|
module Callbacks
|
|
8
|
-
MODELS =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
].freeze,
|
|
34
|
-
T::Array[String],
|
|
35
|
-
)
|
|
8
|
+
MODELS = [
|
|
9
|
+
"before_validation",
|
|
10
|
+
"after_validation",
|
|
11
|
+
"before_save",
|
|
12
|
+
"around_save",
|
|
13
|
+
"after_save",
|
|
14
|
+
"before_create",
|
|
15
|
+
"around_create",
|
|
16
|
+
"after_create",
|
|
17
|
+
"after_commit",
|
|
18
|
+
"after_create_commit",
|
|
19
|
+
"after_update_commit",
|
|
20
|
+
"after_destroy_commit",
|
|
21
|
+
"after_save_commit",
|
|
22
|
+
"after_rollback",
|
|
23
|
+
"before_update",
|
|
24
|
+
"around_update",
|
|
25
|
+
"after_update",
|
|
26
|
+
"before_destroy",
|
|
27
|
+
"around_destroy",
|
|
28
|
+
"after_destroy",
|
|
29
|
+
"after_initialize",
|
|
30
|
+
"after_find",
|
|
31
|
+
"after_touch",
|
|
32
|
+
].freeze
|
|
36
33
|
|
|
37
|
-
CONTROLLERS =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
].freeze,
|
|
52
|
-
T::Array[String],
|
|
53
|
-
)
|
|
34
|
+
CONTROLLERS = [
|
|
35
|
+
"after_action",
|
|
36
|
+
"append_after_action",
|
|
37
|
+
"append_around_action",
|
|
38
|
+
"append_before_action",
|
|
39
|
+
"around_action",
|
|
40
|
+
"before_action",
|
|
41
|
+
"prepend_after_action",
|
|
42
|
+
"prepend_around_action",
|
|
43
|
+
"prepend_before_action",
|
|
44
|
+
"skip_after_action",
|
|
45
|
+
"skip_around_action",
|
|
46
|
+
"skip_before_action",
|
|
47
|
+
].freeze
|
|
54
48
|
|
|
55
|
-
JOBS =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
].freeze,
|
|
64
|
-
T::Array[String],
|
|
65
|
-
)
|
|
49
|
+
JOBS = [
|
|
50
|
+
"after_enqueue",
|
|
51
|
+
"after_perform",
|
|
52
|
+
"around_enqueue",
|
|
53
|
+
"around_perform",
|
|
54
|
+
"before_enqueue",
|
|
55
|
+
"before_perform",
|
|
56
|
+
].freeze
|
|
66
57
|
|
|
67
|
-
|
|
58
|
+
MAILBOX = [
|
|
59
|
+
"after_processing",
|
|
60
|
+
"before_processing",
|
|
61
|
+
"around_processing",
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
ALL = (MODELS + CONTROLLERS + JOBS + MAILBOX).freeze #: Array[String]
|
|
68
65
|
end
|
|
69
66
|
end
|
|
70
67
|
end
|
|
@@ -6,9 +6,7 @@ module RubyLsp
|
|
|
6
6
|
module Support
|
|
7
7
|
class LocationBuilder
|
|
8
8
|
class << self
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
sig { params(location_string: String).returns(Interface::Location) }
|
|
9
|
+
#: (String location_string) -> Interface::Location
|
|
12
10
|
def line_location_from_s(location_string)
|
|
13
11
|
*file_parts, line = location_string.split(":")
|
|
14
12
|
raise ArgumentError, "Invalid location string given" if file_parts.empty?
|