ruby-lsp-rails 0.3.16 → 0.3.18

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bab18094b00d6c5e6a61b22061f927fbca1f4b57ce40da04eca22081ab264385
4
- data.tar.gz: 7a253986008708cb5d5af08b6409039aeba2b54d0d605270f652fba9178a99ac
3
+ metadata.gz: 159bd26f9edd1f611d7f13cb5864e63d8ecc1c6180bbf5a426b9e40a98ce25ba
4
+ data.tar.gz: 5266ed45904e0d62f074684a3a943ac1ef66b7827c4177ee7631d5ae46118553
5
5
  SHA512:
6
- metadata.gz: b5021b0f2dadbb18464e473f26d9bfee94ed22b02b53eec5c1c44ade37f1ab989c0b80c4169f722ca4e38c498344ccd5e19334324508aac8f6d3c0e21bd6ec35
7
- data.tar.gz: 26813fab8da91e99d6f708dadc1ef631df0d1791addb608e06a366bfebcac5643c5d0978d0bc0778d76c3d74572a78a0a8b361fba90f373db759b54a81f9a388
6
+ metadata.gz: ab185650a7d5d24f1d7e7faf29cea5eabc99d905525ea840b2000003059393c60be8b79ef9f97ef3a78bf58ae695a5d5f19aa5bbecc9ea367d48090c9614538c
7
+ data.tar.gz: ef04db908af2f8ce9ee58a0059e51736f969f487b769a421d7df141cfc0518f31c2aa503eace5cc5ad24db36f56a1855ba55ffc4d5990f1eccc5d8cea1a4931c
data/README.md CHANGED
@@ -1,20 +1,20 @@
1
1
  [![Ruby DX Slack](https://img.shields.io/badge/Slack-Ruby%20DX-success?logo=slack)](https://join.slack.com/t/ruby-dx/shared_invite/zt-2c8zjlir6-uUDJl8oIwcen_FS_aA~b6Q)
2
2
 
3
- # Rails addon
3
+ # Rails add-on
4
4
 
5
- The Rails addon is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) [addon](https://shopify.github.io/ruby-lsp/addons.html) for extra [Rails editor features](https://shopify.github.io/ruby-lsp/rails-addon.html).
5
+ The Rails add-on is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) [add-on](https://shopify.github.io/ruby-lsp/add-ons.html) for extra [Rails editor features](https://shopify.github.io/ruby-lsp/rails-add-on.html).
6
6
 
7
7
  ## Installation
8
8
 
9
9
  If you haven't already done so, you'll need to first [set up Ruby LSP](https://shopify.github.io/ruby-lsp/#usage).
10
10
 
11
- As of v0.3.0, Ruby LSP will automatically include the Ruby LSP Rails addon in its custom bundle when a Rails app is detected.
11
+ As of v0.3.0, Ruby LSP will automatically include the Ruby LSP Rails add-on in its custom bundle when a Rails app is detected.
12
12
  There is no need to add the gem to your bundle.
13
13
 
14
14
  ## Documentation
15
15
 
16
- See the [documentation](https://shopify.github.io/ruby-lsp/rails-addon.html) for more in-depth details about the
17
- [supported features](https://shopify.github.io/ruby-lsp/rails-addon.html#features).
16
+ See the [documentation](https://shopify.github.io/ruby-lsp/rails-add-on.html) for more in-depth details about the
17
+ [supported features](https://shopify.github.io/ruby-lsp/rails-add-on.html#features).
18
18
 
19
19
  ## Contributing
20
20
 
@@ -28,20 +28,35 @@ 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))
32
+ @addon_mutex = T.let(Mutex.new, Mutex)
33
+ @client_mutex = T.let(Mutex.new, Mutex)
34
+ @client_mutex.lock
35
+
36
+ Thread.new do
37
+ @addon_mutex.synchronize do
38
+ # We need to ensure the Rails client is fully loaded before we activate the server addons
39
+ @client_mutex.synchronize { @rails_runner_client = RunnerClient.create_client(T.must(@outgoing_queue)) }
40
+ end
41
+ end
31
42
  end
32
43
 
33
44
  sig { returns(RunnerClient) }
34
- attr_reader :rails_runner_client
45
+ def rails_runner_client
46
+ @addon_mutex.synchronize { @rails_runner_client }
47
+ end
35
48
 
36
- sig { override.params(global_state: GlobalState, message_queue: Thread::Queue).void }
37
- 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)
38
51
  @global_state = global_state
39
- $stderr.puts("Activating Ruby LSP Rails addon v#{VERSION}")
40
- # 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 }
42
- 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}")
43
54
 
55
+ register_additional_file_watchers(global_state: global_state, outgoing_queue: outgoing_queue)
44
56
  @global_state.index.register_enhancement(IndexingEnhancement.new)
57
+
58
+ # Start booting the real client in a background thread. Until this completes, the client will be a NullClient
59
+ @client_mutex.unlock
45
60
  end
46
61
 
47
62
  sig { override.void }
@@ -49,6 +64,11 @@ module RubyLsp
49
64
  @rails_runner_client.shutdown
50
65
  end
51
66
 
67
+ sig { override.returns(String) }
68
+ def version
69
+ VERSION
70
+ end
71
+
52
72
  # Creates a new CodeLens listener. This method is invoked on every CodeLens request
53
73
  sig do
54
74
  override.params(
@@ -106,11 +126,11 @@ module RubyLsp
106
126
  end
107
127
  end
108
128
 
109
- sig { params(global_state: GlobalState, message_queue: Thread::Queue).void }
110
- 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:)
111
131
  return unless global_state.supports_watching_files
112
132
 
113
- message_queue << Request.new(
133
+ outgoing_queue << Request.new(
114
134
  id: "ruby-lsp-rails-file-watcher",
115
135
  method: "client/registerCapability",
116
136
  params: Interface::RegistrationParams.new(
@@ -198,9 +198,13 @@ module RubyLsp
198
198
  .gsub("::", "/")
199
199
  .downcase
200
200
 
201
- view_uris = Dir.glob("#{@client.rails_root}/app/views/#{controller_name}/#{action_name}*").map! do |path|
201
+ view_uris = Dir.glob("#{@client.rails_root}/app/views/#{controller_name}/#{action_name}*").filter_map do |path|
202
+ # it's possible we could have a directory with the same name as the action, so we need to skip those
203
+ next if File.directory?(path)
204
+
202
205
  URI::Generic.from_path(path: path).to_s
203
206
  end
207
+
204
208
  return if view_uris.empty?
205
209
 
206
210
  @response_builder << create_code_lens(
@@ -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
@@ -60,6 +60,7 @@ module RubyLsp
60
60
  name_arg.location,
61
61
  name_arg.location,
62
62
  nil,
63
+ index.configuration.encoding,
63
64
  [RubyIndexer::Entry::Signature.new([])],
64
65
  RubyIndexer::Entry::Visibility::PUBLIC,
65
66
  owner,
@@ -72,6 +73,7 @@ module RubyLsp
72
73
  name_arg.location,
73
74
  name_arg.location,
74
75
  nil,
76
+ index.configuration.encoding,
75
77
  [RubyIndexer::Entry::Signature.new([RubyIndexer::Entry::RequiredParameter.new(name: name.to_sym)])],
76
78
  RubyIndexer::Entry::Visibility::PUBLIC,
77
79
  owner,
@@ -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,30 +92,52 @@ 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
98
121
 
122
+ sig { params(server_addon_path: String).void }
123
+ def register_server_addon(server_addon_path)
124
+ send_notification("server_addon/register", server_addon_path: server_addon_path)
125
+ rescue IncompleteMessageError
126
+ log_message(
127
+ "Ruby LSP Rails failed to register server addon #{server_addon_path}",
128
+ type: RubyLsp::Constant::MessageType::ERROR,
129
+ )
130
+ nil
131
+ end
132
+
99
133
  sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
100
134
  def model(name)
101
135
  make_request("model", name: name)
102
136
  rescue IncompleteMessageError
103
- $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
+ )
104
141
  nil
105
142
  end
106
143
 
@@ -116,15 +153,22 @@ module RubyLsp
116
153
  model_name: model_name,
117
154
  association_name: association_name,
118
155
  )
119
- rescue => e
120
- $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
121
162
  end
122
163
 
123
164
  sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
124
165
  def route_location(name)
125
166
  make_request("route_location", name: name)
126
167
  rescue IncompleteMessageError
127
- $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
+ )
128
172
  nil
129
173
  end
130
174
 
@@ -132,22 +176,58 @@ module RubyLsp
132
176
  def route(controller:, action:)
133
177
  make_request("route_info", controller: controller, action: action)
134
178
  rescue IncompleteMessageError
135
- $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
136
213
  nil
137
214
  end
138
215
 
139
216
  sig { void }
140
217
  def trigger_reload
141
- $stderr.puts("Reloading Rails application")
218
+ log_message("Reloading Rails application")
142
219
  send_notification("reload")
143
220
  rescue IncompleteMessageError
144
- $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
+ )
145
225
  nil
146
226
  end
147
227
 
148
228
  sig { void }
149
229
  def shutdown
150
- $stderr.puts("Ruby LSP Rails shutting down server")
230
+ log_message("Ruby LSP Rails shutting down server")
151
231
  send_message("shutdown")
152
232
  sleep(0.5) # give the server a bit of time to shutdown
153
233
  [@stdin, @stdout, @stderr].each(&:close)
@@ -164,24 +244,24 @@ module RubyLsp
164
244
  sig do
165
245
  params(
166
246
  request: String,
167
- params: T.nilable(T::Hash[Symbol, T.untyped]),
247
+ params: T.untyped,
168
248
  ).returns(T.nilable(T::Hash[Symbol, T.untyped]))
169
249
  end
170
- def make_request(request, params = nil)
171
- send_message(request, params)
250
+ def make_request(request, **params)
251
+ send_message(request, **params)
172
252
  read_response
173
253
  end
174
254
 
175
255
  # 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)
256
+ sig { params(request: String, params: T.untyped).void }
257
+ def send_notification(request, **params) = send_message(request, **params)
178
258
 
179
259
  private
180
260
 
181
- sig { overridable.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
182
- def send_message(request, params = nil)
261
+ sig { overridable.params(request: String, params: T.untyped).void }
262
+ def send_message(request, **params)
183
263
  message = { method: request }
184
- message[:params] = params if params
264
+ message[:params] = params
185
265
  json = message.to_json
186
266
 
187
267
  @mutex.synchronize do
@@ -206,7 +286,10 @@ module RubyLsp
206
286
  response = JSON.parse(T.must(raw_response), symbolize_names: true)
207
287
 
208
288
  if response[:error]
209
- $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
+ )
210
293
  return
211
294
  end
212
295
 
@@ -221,6 +304,13 @@ module RubyLsp
221
304
  # Windows does not support the `TERM` signal, so we're forced to use `KILL` here
222
305
  Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
223
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
224
314
  end
225
315
 
226
316
  class NullClient < RunnerClient
@@ -247,8 +337,13 @@ module RubyLsp
247
337
 
248
338
  private
249
339
 
250
- sig { override.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
251
- 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)
252
347
  # no-op
253
348
  end
254
349
 
@@ -8,7 +8,63 @@ 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
+
24
+ class ServerAddon
25
+ include Common
26
+
27
+ @server_addon_classes = []
28
+ @server_addons = {}
29
+
30
+ class << self
31
+ # We keep track of runtime server add-ons the same way we track other add-ons, by storing classes that inherit
32
+ # from the base one
33
+ def inherited(child)
34
+ @server_addon_classes << child
35
+ super
36
+ end
37
+
38
+ # Delegate `request` with `params` to the server add-on with the given `name`
39
+ def delegate(name, request, params)
40
+ @server_addons[name]&.execute(request, params)
41
+ end
42
+
43
+ # Instantiate all server addons and store them in a hash for easy access after we have discovered the classes
44
+ def finalize_registrations!(stdout)
45
+ until @server_addon_classes.empty?
46
+ addon = @server_addon_classes.shift.new(stdout)
47
+ @server_addons[addon.name] = addon
48
+ end
49
+ end
50
+ end
51
+
52
+ def initialize(stdout)
53
+ @stdout = stdout
54
+ end
55
+
56
+ def name
57
+ raise NotImplementedError, "Not implemented!"
58
+ end
59
+
60
+ def execute(request, params)
61
+ raise NotImplementedError, "Not implemented!"
62
+ end
63
+ end
64
+
11
65
  class Server
66
+ include Common
67
+
12
68
  def initialize(stdout: $stdout, override_default_output_device: true)
13
69
  # Grab references to the original pipes so that we can change the default output device further down
14
70
  @stdin = $stdin
@@ -34,8 +90,7 @@ module RubyLsp
34
90
  routes_reloader = ::Rails.application.routes_reloader
35
91
  routes_reloader.execute_unless_loaded if routes_reloader&.respond_to?(:execute_unless_loaded)
36
92
 
37
- initialize_result = { result: { message: "ok", root: ::Rails.root.to_s } }.to_json
38
- @stdout.write("Content-Length: #{initialize_result.length}\r\n\r\n#{initialize_result}")
93
+ send_message({ result: { message: "ok", root: ::Rails.root.to_s } })
39
94
 
40
95
  while @running
41
96
  headers = @stdin.gets("\r\n\r\n")
@@ -51,27 +106,29 @@ module RubyLsp
51
106
  when "shutdown"
52
107
  @running = false
53
108
  when "model"
54
- write_response(resolve_database_info_from_model(params.fetch(:name)))
109
+ send_message(resolve_database_info_from_model(params.fetch(:name)))
55
110
  when "association_target_location"
56
- write_response(resolve_association_target(params))
111
+ send_message(resolve_association_target(params))
57
112
  when "reload"
58
113
  ::Rails.application.reloader.reload!
59
114
  when "route_location"
60
- write_response(route_location(params.fetch(:name)))
115
+ send_message(route_location(params.fetch(:name)))
61
116
  when "route_info"
62
- write_response(resolve_route_info(params))
117
+ send_message(resolve_route_info(params))
118
+ when "server_addon/register"
119
+ require params[:server_addon_path]
120
+ ServerAddon.finalize_registrations!(@stdout)
121
+ when "server_addon/delegate"
122
+ server_addon_name = params.delete(:server_addon_name)
123
+ request_name = params.delete(:request_name)
124
+ ServerAddon.delegate(server_addon_name, request_name, params)
63
125
  end
64
126
  rescue => e
65
- write_response({ error: e.full_message(highlight: false) })
127
+ send_message({ error: e.full_message(highlight: false) })
66
128
  end
67
129
 
68
130
  private
69
131
 
70
- def write_response(response)
71
- json_response = response.to_json
72
- @stdout.write("Content-Length: #{json_response.length}\r\n\r\n#{json_response}")
73
- end
74
-
75
132
  def resolve_route_info(requirements)
76
133
  if requirements[:controller]
77
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.16"
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.16
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-09-19 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
@@ -16,20 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.18.0
19
+ version: 0.19.0
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: 0.19.0
22
+ version: 0.20.0
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 0.18.0
29
+ version: 0.19.0
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: 0.19.0
32
+ version: 0.20.0
33
33
  description: A Ruby LSP addon that adds extra editor functionality for Rails applications
34
34
  email:
35
35
  - ruby@shopify.com
@@ -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
@@ -65,7 +64,7 @@ metadata:
65
64
  homepage_uri: https://github.com/Shopify/ruby-lsp-rails
66
65
  source_code_uri: https://github.com/Shopify/ruby-lsp-rails
67
66
  changelog_uri: https://github.com/Shopify/ruby-lsp-rails/releases
68
- documentation_uri: https://shopify.github.io/ruby-lsp/rails-addon.html
67
+ documentation_uri: https://shopify.github.io/ruby-lsp/rails-add-on.html
69
68
  post_install_message:
70
69
  rdoc_options: []
71
70
  require_paths:
@@ -81,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
80
  - !ruby/object:Gem::Version
82
81
  version: '0'
83
82
  requirements: []
84
- rubygems_version: 3.5.18
83
+ rubygems_version: 3.5.20
85
84
  signing_key:
86
85
  specification_version: 4
87
86
  summary: A Ruby LSP addon for Rails
@@ -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