ruby-lsp-rails 0.3.17 → 0.3.18

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba36df033667789339419a3ed19740d35ad9b91a3463cf2438bd390c4580aeb4
4
- data.tar.gz: 7aa58db6217944f945ca3bb9bcdefeb4f018759acf8673e222ae8fb3b5c523ee
3
+ metadata.gz: 159bd26f9edd1f611d7f13cb5864e63d8ecc1c6180bbf5a426b9e40a98ce25ba
4
+ data.tar.gz: 5266ed45904e0d62f074684a3a943ac1ef66b7827c4177ee7631d5ae46118553
5
5
  SHA512:
6
- metadata.gz: a4ddf589e0065c32ee9087a4cfd0347d4e3dbdf5d1d667f1ca6a440ac1c6b2d1c18f41acfa2264f72114ecc4bfafec4c3c40748a3f6639a80c86093ca8e51cef
7
- data.tar.gz: 2dbb86bd0f701865f3d833fb13079cfd9bd27310cdca2410664c7112db73e9915c9b633de1f527220a64055d3956009a738c367c956c3c08cd5c3ae71e04d7f1
6
+ metadata.gz: ab185650a7d5d24f1d7e7faf29cea5eabc99d905525ea840b2000003059393c60be8b79ef9f97ef3a78bf58ae695a5d5f19aa5bbecc9ea367d48090c9614538c
7
+ data.tar.gz: ef04db908af2f8ce9ee58a0059e51736f969f487b769a421d7df141cfc0518f31c2aa503eace5cc5ad24db36f56a1855ba55ffc4d5990f1eccc5d8cea1a4931c
@@ -28,6 +28,7 @@ module RubyLsp
28
28
  # the real client is initialized, features that depend on it will not be blocked by using the NullClient
29
29
  @rails_runner_client = T.let(NullClient.new, RunnerClient)
30
30
  @global_state = T.let(nil, T.nilable(GlobalState))
31
+ @outgoing_queue = T.let(nil, T.nilable(Thread::Queue))
31
32
  @addon_mutex = T.let(Mutex.new, Mutex)
32
33
  @client_mutex = T.let(Mutex.new, Mutex)
33
34
  @client_mutex.lock
@@ -35,7 +36,7 @@ module RubyLsp
35
36
  Thread.new do
36
37
  @addon_mutex.synchronize do
37
38
  # We need to ensure the Rails client is fully loaded before we activate the server addons
38
- @client_mutex.synchronize { @rails_runner_client = RunnerClient.create_client }
39
+ @client_mutex.synchronize { @rails_runner_client = RunnerClient.create_client(T.must(@outgoing_queue)) }
39
40
  end
40
41
  end
41
42
  end
@@ -45,11 +46,13 @@ module RubyLsp
45
46
  @addon_mutex.synchronize { @rails_runner_client }
46
47
  end
47
48
 
48
- sig { override.params(global_state: GlobalState, message_queue: Thread::Queue).void }
49
- def activate(global_state, message_queue)
49
+ sig { override.params(global_state: GlobalState, outgoing_queue: Thread::Queue).void }
50
+ def activate(global_state, outgoing_queue)
50
51
  @global_state = global_state
51
- $stderr.puts("Activating Ruby LSP Rails add-on v#{version}")
52
- register_additional_file_watchers(global_state: global_state, message_queue: message_queue)
52
+ @outgoing_queue = outgoing_queue
53
+ @outgoing_queue << Notification.window_log_message("Activating Ruby LSP Rails add-on v#{VERSION}")
54
+
55
+ register_additional_file_watchers(global_state: global_state, outgoing_queue: outgoing_queue)
53
56
  @global_state.index.register_enhancement(IndexingEnhancement.new)
54
57
 
55
58
  # Start booting the real client in a background thread. Until this completes, the client will be a NullClient
@@ -123,11 +126,11 @@ module RubyLsp
123
126
  end
124
127
  end
125
128
 
126
- sig { params(global_state: GlobalState, message_queue: Thread::Queue).void }
127
- def register_additional_file_watchers(global_state:, message_queue:)
129
+ sig { params(global_state: GlobalState, outgoing_queue: Thread::Queue).void }
130
+ def register_additional_file_watchers(global_state:, outgoing_queue:)
128
131
  return unless global_state.supports_watching_files
129
132
 
130
- message_queue << Request.new(
133
+ outgoing_queue << Request.new(
131
134
  id: "ruby-lsp-rails-file-watcher",
132
135
  method: "client/registerCapability",
133
136
  params: Interface::RegistrationParams.new(
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "support/rails_document_client"
5
-
6
4
  module RubyLsp
7
5
  module Rails
8
6
  # ![Hover demo](../../hover.gif)
@@ -34,7 +32,7 @@ module RubyLsp
34
32
  @response_builder = response_builder
35
33
  @nesting = T.let(node_context.nesting, T::Array[String])
36
34
  @index = T.let(global_state.index, RubyIndexer::Index)
37
- dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter, :on_call_node_enter)
35
+ dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter)
38
36
  end
39
37
 
40
38
  sig { params(node: Prism::ConstantPathNode).void }
@@ -43,10 +41,7 @@ module RubyLsp
43
41
  return unless entries
44
42
 
45
43
  name = T.must(entries.first).name
46
-
47
44
  generate_column_content(name)
48
-
49
- generate_rails_document_link_hover(name, node.location)
50
45
  end
51
46
 
52
47
  sig { params(node: Prism::ConstantReadNode).void }
@@ -57,16 +52,6 @@ module RubyLsp
57
52
  generate_column_content(T.must(entries.first).name)
58
53
  end
59
54
 
60
- sig { params(node: Prism::CallNode).void }
61
- def on_call_node_enter(node)
62
- message_value = node.message
63
- message_loc = node.message_loc
64
-
65
- return unless message_value && message_loc
66
-
67
- generate_rails_document_link_hover(message_value, message_loc)
68
- end
69
-
70
55
  private
71
56
 
72
57
  sig { params(name: String).void }
@@ -77,8 +62,8 @@ module RubyLsp
77
62
  schema_file = model[:schema_file]
78
63
 
79
64
  @response_builder.push(
80
- "[Schema](#{URI::Generic.from_path(path: schema_file)})",
81
- category: :links,
65
+ "[Schema](#{URI::Generic.from_path(path: schema_file)})\n",
66
+ category: :documentation,
82
67
  ) if schema_file
83
68
 
84
69
  @response_builder.push(
@@ -89,14 +74,6 @@ module RubyLsp
89
74
  category: :documentation,
90
75
  )
91
76
  end
92
-
93
- sig { params(name: String, location: Prism::Location).void }
94
- def generate_rails_document_link_hover(name, location)
95
- urls = Support::RailsDocumentClient.generate_rails_document_urls(name)
96
- return if urls.empty?
97
-
98
- @response_builder.push(urls.join("\n\n"), category: :links)
99
- end
100
77
  end
101
78
  end
102
79
  end
@@ -10,20 +10,34 @@ module RubyLsp
10
10
  class << self
11
11
  extend T::Sig
12
12
 
13
- sig { returns(RunnerClient) }
14
- def create_client
13
+ sig { params(outgoing_queue: Thread::Queue).returns(RunnerClient) }
14
+ def create_client(outgoing_queue)
15
15
  if File.exist?("bin/rails")
16
- new
16
+ new(outgoing_queue)
17
17
  else
18
- $stderr.puts(<<~MSG)
19
- Ruby LSP Rails failed to locate bin/rails in the current directory: #{Dir.pwd}"
20
- MSG
21
- $stderr.puts("Server dependent features will not be available")
18
+ unless outgoing_queue.closed?
19
+ outgoing_queue << RubyLsp::Notification.window_log_message(
20
+ <<~MESSAGE.chomp,
21
+ Ruby LSP Rails failed to locate bin/rails in the current directory: #{Dir.pwd}
22
+ Server dependent features will not be available
23
+ MESSAGE
24
+ type: RubyLsp::Constant::MessageType::WARNING,
25
+ )
26
+ end
27
+
22
28
  NullClient.new
23
29
  end
24
30
  rescue Errno::ENOENT, StandardError => e # rubocop:disable Lint/ShadowedException
25
- $stderr.puts("Ruby LSP Rails failed to initialize server: #{e.message}\n#{e.backtrace&.join("\n")}")
26
- $stderr.puts("Server dependent features will not be available")
31
+ unless outgoing_queue.closed?
32
+ outgoing_queue << RubyLsp::Notification.window_log_message(
33
+ <<~MESSAGE.chomp,
34
+ Ruby LSP Rails failed to initialize server: #{e.full_message}
35
+ Server dependent features will not be available
36
+ MESSAGE
37
+ type: Constant::MessageType::ERROR,
38
+ )
39
+ end
40
+
27
41
  NullClient.new
28
42
  end
29
43
  end
@@ -39,8 +53,9 @@ module RubyLsp
39
53
  sig { returns(String) }
40
54
  attr_reader :rails_root
41
55
 
42
- sig { void }
43
- def initialize
56
+ sig { params(outgoing_queue: Thread::Queue).void }
57
+ def initialize(outgoing_queue)
58
+ @outgoing_queue = T.let(outgoing_queue, Thread::Queue)
44
59
  @mutex = T.let(Mutex.new, Mutex)
45
60
  # Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
46
61
  # parent ends, the spring process ends as well. If this is not set, Spring will throw an error while trying to
@@ -69,7 +84,7 @@ module RubyLsp
69
84
  @stdout.binmode
70
85
  @stderr.binmode
71
86
 
72
- $stderr.puts("Ruby LSP Rails booting server")
87
+ log_message("Ruby LSP Rails booting server")
73
88
  count = 0
74
89
 
75
90
  begin
@@ -77,21 +92,29 @@ module RubyLsp
77
92
  initialize_response = T.must(read_response)
78
93
  @rails_root = T.let(initialize_response[:root], String)
79
94
  rescue EmptyMessageError
80
- $stderr.puts("Ruby LSP Rails is retrying initialize (#{count})")
95
+ log_message("Ruby LSP Rails is retrying initialize (#{count})")
81
96
  retry if count < MAX_RETRIES
82
97
  end
83
98
 
84
- $stderr.puts("Finished booting Ruby LSP Rails server")
99
+ log_message("Finished booting Ruby LSP Rails server")
85
100
 
86
101
  unless ENV["RAILS_ENV"] == "test"
87
102
  at_exit do
88
103
  if @wait_thread.alive?
89
- $stderr.puts("Ruby LSP Rails is force killing the server")
90
104
  sleep(0.5) # give the server a bit of time if we already issued a shutdown notification
91
105
  force_kill
92
106
  end
93
107
  end
94
108
  end
109
+
110
+ @logger_thread = T.let(
111
+ Thread.new do
112
+ while (content = @stderr.gets("\n"))
113
+ log_message(content, type: RubyLsp::Constant::MessageType::LOG)
114
+ end
115
+ end,
116
+ Thread,
117
+ )
95
118
  rescue Errno::EPIPE, IncompleteMessageError
96
119
  raise InitializationError, @stderr.read
97
120
  end
@@ -100,7 +123,10 @@ module RubyLsp
100
123
  def register_server_addon(server_addon_path)
101
124
  send_notification("server_addon/register", server_addon_path: server_addon_path)
102
125
  rescue IncompleteMessageError
103
- $stderr.puts("Ruby LSP Rails failed to register server addon #{server_addon_path}")
126
+ log_message(
127
+ "Ruby LSP Rails failed to register server addon #{server_addon_path}",
128
+ type: RubyLsp::Constant::MessageType::ERROR,
129
+ )
104
130
  nil
105
131
  end
106
132
 
@@ -108,7 +134,10 @@ module RubyLsp
108
134
  def model(name)
109
135
  make_request("model", name: name)
110
136
  rescue IncompleteMessageError
111
- $stderr.puts("Ruby LSP Rails failed to get model information: #{@stderr.read}")
137
+ log_message(
138
+ "Ruby LSP Rails failed to get model information",
139
+ type: RubyLsp::Constant::MessageType::ERROR,
140
+ )
112
141
  nil
113
142
  end
114
143
 
@@ -124,15 +153,22 @@ module RubyLsp
124
153
  model_name: model_name,
125
154
  association_name: association_name,
126
155
  )
127
- rescue => e
128
- $stderr.puts("Ruby LSP Rails failed with #{e.message}: #{@stderr.read}")
156
+ rescue IncompleteMessageError
157
+ log_message(
158
+ "Ruby LSP Rails failed to get association location",
159
+ type: RubyLsp::Constant::MessageType::ERROR,
160
+ )
161
+ nil
129
162
  end
130
163
 
131
164
  sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
132
165
  def route_location(name)
133
166
  make_request("route_location", name: name)
134
167
  rescue IncompleteMessageError
135
- $stderr.puts("Ruby LSP Rails failed to get route location: #{@stderr.read}")
168
+ log_message(
169
+ "Ruby LSP Rails failed to get route location",
170
+ type: RubyLsp::Constant::MessageType::ERROR,
171
+ )
136
172
  nil
137
173
  end
138
174
 
@@ -140,22 +176,58 @@ module RubyLsp
140
176
  def route(controller:, action:)
141
177
  make_request("route_info", controller: controller, action: action)
142
178
  rescue IncompleteMessageError
143
- $stderr.puts("Ruby LSP Rails failed to get route information: #{@stderr.read}")
179
+ log_message(
180
+ "Ruby LSP Rails failed to get route information",
181
+ type: RubyLsp::Constant::MessageType::ERROR,
182
+ )
183
+ nil
184
+ end
185
+
186
+ # Delegates a notification to a server add-on
187
+ sig { params(server_addon_name: String, request_name: String, params: T.untyped).void }
188
+ def delegate_notification(server_addon_name:, request_name:, **params)
189
+ send_notification(
190
+ "server_addon/delegate",
191
+ request_name: request_name,
192
+ server_addon_name: server_addon_name,
193
+ **params,
194
+ )
195
+ end
196
+
197
+ # Delegates a request to a server add-on
198
+ sig do
199
+ params(
200
+ server_addon_name: String,
201
+ request_name: String,
202
+ params: T.untyped,
203
+ ).returns(T.nilable(T::Hash[Symbol, T.untyped]))
204
+ end
205
+ def delegate_request(server_addon_name:, request_name:, **params)
206
+ make_request(
207
+ "server_addon/delegate",
208
+ server_addon_name: server_addon_name,
209
+ request_name: request_name,
210
+ **params,
211
+ )
212
+ rescue IncompleteMessageError
144
213
  nil
145
214
  end
146
215
 
147
216
  sig { void }
148
217
  def trigger_reload
149
- $stderr.puts("Reloading Rails application")
218
+ log_message("Reloading Rails application")
150
219
  send_notification("reload")
151
220
  rescue IncompleteMessageError
152
- $stderr.puts("Ruby LSP Rails failed to trigger reload")
221
+ log_message(
222
+ "Ruby LSP Rails failed to trigger reload",
223
+ type: RubyLsp::Constant::MessageType::ERROR,
224
+ )
153
225
  nil
154
226
  end
155
227
 
156
228
  sig { void }
157
229
  def shutdown
158
- $stderr.puts("Ruby LSP Rails shutting down server")
230
+ log_message("Ruby LSP Rails shutting down server")
159
231
  send_message("shutdown")
160
232
  sleep(0.5) # give the server a bit of time to shutdown
161
233
  [@stdin, @stdout, @stderr].each(&:close)
@@ -172,24 +244,24 @@ module RubyLsp
172
244
  sig do
173
245
  params(
174
246
  request: String,
175
- params: T.nilable(T::Hash[Symbol, T.untyped]),
247
+ params: T.untyped,
176
248
  ).returns(T.nilable(T::Hash[Symbol, T.untyped]))
177
249
  end
178
- def make_request(request, params = nil)
179
- send_message(request, params)
250
+ def make_request(request, **params)
251
+ send_message(request, **params)
180
252
  read_response
181
253
  end
182
254
 
183
255
  # Notifications are like messages, but one-way, with no response sent back.
184
- sig { params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
185
- def send_notification(request, params = nil) = send_message(request, params)
256
+ sig { params(request: String, params: T.untyped).void }
257
+ def send_notification(request, **params) = send_message(request, **params)
186
258
 
187
259
  private
188
260
 
189
- sig { overridable.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
190
- def send_message(request, params = nil)
261
+ sig { overridable.params(request: String, params: T.untyped).void }
262
+ def send_message(request, **params)
191
263
  message = { method: request }
192
- message[:params] = params if params
264
+ message[:params] = params
193
265
  json = message.to_json
194
266
 
195
267
  @mutex.synchronize do
@@ -214,7 +286,10 @@ module RubyLsp
214
286
  response = JSON.parse(T.must(raw_response), symbolize_names: true)
215
287
 
216
288
  if response[:error]
217
- $stderr.puts("Ruby LSP Rails error: " + response[:error])
289
+ log_message(
290
+ "Ruby LSP Rails error: #{response[:error]}",
291
+ type: RubyLsp::Constant::MessageType::ERROR,
292
+ )
218
293
  return
219
294
  end
220
295
 
@@ -229,6 +304,13 @@ module RubyLsp
229
304
  # Windows does not support the `TERM` signal, so we're forced to use `KILL` here
230
305
  Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
231
306
  end
307
+
308
+ sig { params(message: ::String, type: ::Integer).void }
309
+ def log_message(message, type: RubyLsp::Constant::MessageType::LOG)
310
+ return if @outgoing_queue.closed?
311
+
312
+ @outgoing_queue << RubyLsp::Notification.window_log_message(message, type: type)
313
+ end
232
314
  end
233
315
 
234
316
  class NullClient < RunnerClient
@@ -255,8 +337,13 @@ module RubyLsp
255
337
 
256
338
  private
257
339
 
258
- sig { override.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
259
- def send_message(request, params = nil)
340
+ sig { params(message: ::String, type: ::Integer).void }
341
+ def log_message(message, type: RubyLsp::Constant::MessageType::LOG)
342
+ # no-op
343
+ end
344
+
345
+ sig { override.params(request: String, params: T.untyped).void }
346
+ def send_message(request, **params)
260
347
  # no-op
261
348
  end
262
349
 
@@ -8,7 +8,22 @@ require "json"
8
8
 
9
9
  module RubyLsp
10
10
  module Rails
11
+ module Common
12
+ # Write a message to the client. Can be used for sending notifications to the editor
13
+ def send_message(message)
14
+ json_message = message.to_json
15
+ @stdout.write("Content-Length: #{json_message.length}\r\n\r\n#{json_message}")
16
+ end
17
+
18
+ # Log a debug message to the editor's output
19
+ def debug_message(message)
20
+ $stderr.puts(message)
21
+ end
22
+ end
23
+
11
24
  class ServerAddon
25
+ include Common
26
+
12
27
  @server_addon_classes = []
13
28
  @server_addons = {}
14
29
 
@@ -38,12 +53,6 @@ module RubyLsp
38
53
  @stdout = stdout
39
54
  end
40
55
 
41
- # Write a response back. Can be used for sending notifications to the editor
42
- def write_response(response)
43
- json_response = response.to_json
44
- @stdout.write("Content-Length: #{json_response.length}\r\n\r\n#{json_response}")
45
- end
46
-
47
56
  def name
48
57
  raise NotImplementedError, "Not implemented!"
49
58
  end
@@ -54,6 +63,8 @@ module RubyLsp
54
63
  end
55
64
 
56
65
  class Server
66
+ include Common
67
+
57
68
  def initialize(stdout: $stdout, override_default_output_device: true)
58
69
  # Grab references to the original pipes so that we can change the default output device further down
59
70
  @stdin = $stdin
@@ -79,8 +90,7 @@ module RubyLsp
79
90
  routes_reloader = ::Rails.application.routes_reloader
80
91
  routes_reloader.execute_unless_loaded if routes_reloader&.respond_to?(:execute_unless_loaded)
81
92
 
82
- initialize_result = { result: { message: "ok", root: ::Rails.root.to_s } }.to_json
83
- @stdout.write("Content-Length: #{initialize_result.length}\r\n\r\n#{initialize_result}")
93
+ send_message({ result: { message: "ok", root: ::Rails.root.to_s } })
84
94
 
85
95
  while @running
86
96
  headers = @stdin.gets("\r\n\r\n")
@@ -96,15 +106,15 @@ module RubyLsp
96
106
  when "shutdown"
97
107
  @running = false
98
108
  when "model"
99
- write_response(resolve_database_info_from_model(params.fetch(:name)))
109
+ send_message(resolve_database_info_from_model(params.fetch(:name)))
100
110
  when "association_target_location"
101
- write_response(resolve_association_target(params))
111
+ send_message(resolve_association_target(params))
102
112
  when "reload"
103
113
  ::Rails.application.reloader.reload!
104
114
  when "route_location"
105
- write_response(route_location(params.fetch(:name)))
115
+ send_message(route_location(params.fetch(:name)))
106
116
  when "route_info"
107
- write_response(resolve_route_info(params))
117
+ send_message(resolve_route_info(params))
108
118
  when "server_addon/register"
109
119
  require params[:server_addon_path]
110
120
  ServerAddon.finalize_registrations!(@stdout)
@@ -114,16 +124,11 @@ module RubyLsp
114
124
  ServerAddon.delegate(server_addon_name, request_name, params)
115
125
  end
116
126
  rescue => e
117
- write_response({ error: e.full_message(highlight: false) })
127
+ send_message({ error: e.full_message(highlight: false) })
118
128
  end
119
129
 
120
130
  private
121
131
 
122
- def write_response(response)
123
- json_response = response.to_json
124
- @stdout.write("Content-Length: #{json_response.length}\r\n\r\n#{json_response}")
125
- end
126
-
127
132
  def resolve_route_info(requirements)
128
133
  if requirements[:controller]
129
134
  requirements[:controller] = requirements.fetch(:controller).underscore.delete_suffix("_controller")
@@ -3,6 +3,6 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Rails
6
- VERSION = "0.3.17"
6
+ VERSION = "0.3.18"
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.17
4
+ version: 0.3.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-03 00:00:00.000000000 Z
11
+ date: 2024-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-lsp
@@ -53,7 +53,6 @@ files:
53
53
  - lib/ruby_lsp/ruby_lsp_rails/support/associations.rb
54
54
  - lib/ruby_lsp/ruby_lsp_rails/support/callbacks.rb
55
55
  - lib/ruby_lsp/ruby_lsp_rails/support/location_builder.rb
56
- - lib/ruby_lsp/ruby_lsp_rails/support/rails_document_client.rb
57
56
  - lib/ruby_lsp_rails/railtie.rb
58
57
  - lib/ruby_lsp_rails/version.rb
59
58
  - lib/tasks/ruby_lsp_rails_tasks.rake
@@ -1,131 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require "net/http"
5
-
6
- module RubyLsp
7
- module Rails
8
- module Support
9
- class RailsDocumentClient
10
- RAILS_DOC_HOST = "https://api.rubyonrails.org"
11
-
12
- SUPPORTED_RAILS_DOC_NAMESPACES = T.let(
13
- Regexp.union(
14
- /ActionDispatch/,
15
- /ActionController/,
16
- /AbstractController/,
17
- /ActiveRecord/,
18
- /ActiveModel/,
19
- /ActiveStorage/,
20
- /ActionText/,
21
- /ActiveJob/,
22
- ).freeze,
23
- Regexp,
24
- )
25
-
26
- RAILTIES_VERSION = T.let(
27
- [*::Gem::Specification.default_stubs, *::Gem::Specification.stubs].find do |s|
28
- s.name == "railties"
29
- end&.version&.to_s,
30
- T.nilable(String),
31
- )
32
-
33
- class << self
34
- extend T::Sig
35
-
36
- sig { params(name: String).returns(T::Array[String]) }
37
- def generate_rails_document_urls(name)
38
- docs = search_index&.fetch(name, nil)
39
-
40
- return [] unless docs
41
-
42
- docs.map do |doc|
43
- owner = doc[:owner]
44
-
45
- link_name =
46
- # class/module name
47
- if owner == name
48
- name
49
- else
50
- "#{owner}##{name}"
51
- end
52
-
53
- "[Rails Document: `#{link_name}`](#{doc[:url]})"
54
- end
55
- end
56
-
57
- sig { returns(T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]])) }
58
- private def search_index
59
- @rails_documents ||= T.let(
60
- build_search_index,
61
- T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]]),
62
- )
63
- end
64
-
65
- sig { returns(T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]])) }
66
- private def build_search_index
67
- return unless RAILTIES_VERSION
68
-
69
- $stderr.puts("Fetching search index for Rails documentation")
70
-
71
- response = Net::HTTP.get_response(
72
- URI("#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/js/search_index.js"),
73
- { "User-Agent" => "ruby-lsp-rails/#{RubyLsp::Rails::VERSION}" },
74
- )
75
-
76
- body = case response
77
- when Net::HTTPSuccess
78
- $stderr.puts("Finished fetching search index for Rails documentation")
79
- response.body
80
- when Net::HTTPRedirection
81
- # If the version's doc is not found, e.g. Rails main, it'll be redirected
82
- # In this case, we just fetch the latest doc
83
- response = Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/js/search_index.js"))
84
- if response.is_a?(Net::HTTPSuccess)
85
- $stderr.puts("Finished fetching search index for Rails documentation")
86
- response.body
87
- end
88
- else
89
- $stderr.puts("Response failed: #{response.inspect}")
90
- nil
91
- end
92
-
93
- process_search_index(body) if body
94
- rescue StandardError => e
95
- $stderr.puts("Exception occurred when fetching Rails document index: #{e.inspect}")
96
- end
97
-
98
- sig { params(js: String).returns(T::Hash[String, T::Array[T::Hash[Symbol, String]]]) }
99
- private def process_search_index(js)
100
- raw_data = js.sub("var search_data = ", "")
101
- info = JSON.parse(raw_data).dig("index", "info")
102
-
103
- # An entry looks like this:
104
- #
105
- # ["belongs_to", # method or module/class
106
- # "ActiveRecord::Associations::ClassMethods", # method owner
107
- # "classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to", # path to the document
108
- # "(name, scope = nil, **options)", # method's parameters
109
- # "<p>Specifies a one-to-one association with another class..."] # document preview
110
- #
111
- info.each_with_object({}) do |(method_or_class, method_owner, doc_path, _, doc_preview), table|
112
- # If a method doesn't have documentation, there's no need to generate the link to it.
113
- next if doc_preview.nil? || doc_preview.empty?
114
-
115
- # If the method or class/module is not from the supported namespace, reject it
116
- next unless [method_or_class, method_owner].any? do |elem|
117
- elem.match?(SUPPORTED_RAILS_DOC_NAMESPACES)
118
- end
119
-
120
- owner = method_owner.empty? ? method_or_class : method_owner
121
- table[method_or_class] ||= []
122
- # It's possible to have multiple modules defining the same method name. For example,
123
- # both `ActiveRecord::FinderMethods` and `ActiveRecord::Associations::CollectionProxy` defines `#find`
124
- table[method_or_class] << { owner: owner, url: "#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/#{doc_path}" }
125
- end
126
- end
127
- end
128
- end
129
- end
130
- end
131
- end