ruby-lsp-rails 0.3.16 → 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: 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