ruby-lsp-rails 0.3.21 → 0.3.22

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: feb1936ab1a1b20409425b3d644ab0e3c158b6f21a6bdf024d73013ccac28120
4
- data.tar.gz: f32daf2f444a0ca0fa2a55b0d2bdef2d67abfcf40b4bae5cd351eede9b50dc11
3
+ metadata.gz: 87907f659bf3723d803685fb91ee9ff1f83b9c3b63f8c394ac48a4592be40ab6
4
+ data.tar.gz: dc9ba5353f094aa02d4f73a97de484c9a5a6086ad74427083e61366729620462
5
5
  SHA512:
6
- metadata.gz: 72082ba3519fec8b3453201b9ffa9a270d745bdb4abff0e70edce58ad82024aae836a604bcdbdcec73f54faaae8b28e5a646ffec4bd0910c88099d425f3a6606
7
- data.tar.gz: 4c53919cae2b20d3dbd6402721ebe9470ba89ab4a2db24cbc61f04484d8b19c23c7c99017042ac09fff55e4f3cf34c2830890a270245b89011a0691ba6ab11f1
6
+ metadata.gz: 9eb10192041cde7218dc73bbb9a315f3d9ad3c9c19580371839a21744f9bede5d0063d44bb4c78d53815bbd8811b6b5b33cdc05f6e3f14fd84b9afcb603e444f
7
+ data.tar.gz: e97d8e5f5000cade9b6d876f0c2a261e0a91f3a4201ba249dbbf8385503cb237c796507a0c5901d5814bd2d78f3ae898e7ae42fd43bb98898502ddb56cd8cf05
@@ -20,6 +20,8 @@ module RubyLsp
20
20
  class Addon < ::RubyLsp::Addon
21
21
  extend T::Sig
22
22
 
23
+ RUN_MIGRATIONS_TITLE = "Run Migrations"
24
+
23
25
  sig { void }
24
26
  def initialize
25
27
  super
@@ -37,6 +39,7 @@ module RubyLsp
37
39
  @addon_mutex.synchronize do
38
40
  # We need to ensure the Rails client is fully loaded before we activate the server addons
39
41
  @client_mutex.synchronize { @rails_runner_client = RunnerClient.create_client(T.must(@outgoing_queue)) }
42
+ offer_to_run_pending_migrations
40
43
  end
41
44
  end
42
45
  end
@@ -53,7 +56,7 @@ module RubyLsp
53
56
  @outgoing_queue << Notification.window_log_message("Activating Ruby LSP Rails add-on v#{VERSION}")
54
57
 
55
58
  register_additional_file_watchers(global_state: global_state, outgoing_queue: outgoing_queue)
56
- @global_state.index.register_enhancement(IndexingEnhancement.new)
59
+ @global_state.index.register_enhancement(IndexingEnhancement.new(@global_state.index))
57
60
 
58
61
  # Start booting the real client in a background thread. Until this completes, the client will be a NullClient
59
62
  @client_mutex.unlock
@@ -119,16 +122,85 @@ module RubyLsp
119
122
 
120
123
  sig { params(changes: T::Array[{ uri: String, type: Integer }]).void }
121
124
  def workspace_did_change_watched_files(changes)
122
- if changes.any? do |change|
123
- change[:uri].end_with?("db/schema.rb") || change[:uri].end_with?("structure.sql")
124
- end
125
+ if changes.any? { |c| c[:uri].end_with?("db/schema.rb") || c[:uri].end_with?("structure.sql") }
125
126
  @rails_runner_client.trigger_reload
126
127
  end
128
+
129
+ if changes.any? do |c|
130
+ %r{db/migrate/.*\.rb}.match?(c[:uri]) && c[:type] != Constant::FileChangeType::CHANGED
131
+ end
132
+
133
+ offer_to_run_pending_migrations
134
+ end
135
+ end
136
+
137
+ sig { override.returns(String) }
138
+ def name
139
+ "Ruby LSP Rails"
140
+ end
141
+
142
+ sig { override.params(title: String).void }
143
+ def handle_window_show_message_response(title)
144
+ if title == RUN_MIGRATIONS_TITLE
145
+
146
+ begin_progress("run-migrations", "Running Migrations")
147
+ response = @rails_runner_client.run_migrations
148
+
149
+ if response && @outgoing_queue
150
+ if response[:status] == 0
151
+ # Both log the message and show it as part of progress because sometimes running migrations is so fast you
152
+ # can't see the progress notification
153
+ @outgoing_queue << Notification.window_log_message(response[:message])
154
+ report_progress("run-migrations", message: response[:message])
155
+ else
156
+ @outgoing_queue << Notification.window_show_message(
157
+ "Migrations failed to run\n\n#{response[:message]}",
158
+ type: Constant::MessageType::ERROR,
159
+ )
160
+ end
161
+ end
162
+
163
+ end_progress("run-migrations")
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ sig { params(id: String, title: String, percentage: T.nilable(Integer), message: T.nilable(String)).void }
170
+ def begin_progress(id, title, percentage: nil, message: nil)
171
+ return unless @global_state&.client_capabilities&.supports_progress && @outgoing_queue
172
+
173
+ @outgoing_queue << Request.new(
174
+ id: "progress-request-#{id}",
175
+ method: "window/workDoneProgress/create",
176
+ params: Interface::WorkDoneProgressCreateParams.new(token: id),
177
+ )
178
+
179
+ @outgoing_queue << Notification.progress_begin(
180
+ id,
181
+ title,
182
+ percentage: percentage,
183
+ message: "#{percentage}% completed",
184
+ )
185
+ end
186
+
187
+ sig { params(id: String, percentage: T.nilable(Integer), message: T.nilable(String)).void }
188
+ def report_progress(id, percentage: nil, message: nil)
189
+ return unless @global_state&.client_capabilities&.supports_progress && @outgoing_queue
190
+
191
+ @outgoing_queue << Notification.progress_report(id, percentage: percentage, message: message)
192
+ end
193
+
194
+ sig { params(id: String).void }
195
+ def end_progress(id)
196
+ return unless @global_state&.client_capabilities&.supports_progress && @outgoing_queue
197
+
198
+ @outgoing_queue << Notification.progress_end(id)
127
199
  end
128
200
 
129
201
  sig { params(global_state: GlobalState, outgoing_queue: Thread::Queue).void }
130
202
  def register_additional_file_watchers(global_state:, outgoing_queue:)
131
- return unless global_state.supports_watching_files
203
+ return unless global_state.client_capabilities.supports_watching_files
132
204
 
133
205
  outgoing_queue << Request.new(
134
206
  id: "ruby-lsp-rails-file-watcher",
@@ -152,9 +224,26 @@ module RubyLsp
152
224
  )
153
225
  end
154
226
 
155
- sig { override.returns(String) }
156
- def name
157
- "Ruby LSP Rails"
227
+ sig { void }
228
+ def offer_to_run_pending_migrations
229
+ return unless @outgoing_queue
230
+ return unless @global_state&.client_capabilities&.window_show_message_supports_extra_properties
231
+
232
+ migration_message = @rails_runner_client.pending_migrations_message
233
+ return unless migration_message
234
+
235
+ @outgoing_queue << Request.new(
236
+ id: "rails-pending-migrations",
237
+ method: "window/showMessageRequest",
238
+ params: {
239
+ type: Constant::MessageType::INFO,
240
+ message: migration_message,
241
+ actions: [
242
+ { title: RUN_MIGRATIONS_TITLE, addon_name: name, method: "window/showMessageRequest" },
243
+ { title: "Cancel", addon_name: name, method: "window/showMessageRequest" },
244
+ ],
245
+ },
246
+ )
158
247
  end
159
248
  end
160
249
  end
@@ -67,13 +67,29 @@ module RubyLsp
67
67
  ) if schema_file
68
68
 
69
69
  @response_builder.push(
70
- model[:columns].map do |name, type|
70
+ model[:columns].map do |name, type, default_value, nullable|
71
71
  primary_key_suffix = " (PK)" if model[:primary_keys].include?(name)
72
- "**#{name}**: #{type}#{primary_key_suffix}\n"
72
+ suffixes = []
73
+ suffixes << "default: #{format_default(default_value, type)}" if default_value
74
+ suffixes << "not null" unless nullable || primary_key_suffix.present?
75
+ suffix_string = " - #{suffixes.join(" - ")}" if suffixes.any?
76
+ "**#{name}**: #{type}#{primary_key_suffix}#{suffix_string}\n"
73
77
  end.join("\n"),
74
78
  category: :documentation,
75
79
  )
76
80
  end
81
+
82
+ sig { params(default_value: String, type: String).returns(String) }
83
+ def format_default(default_value, type)
84
+ case type
85
+ when "boolean"
86
+ default_value == "true" ? "true" : "false"
87
+ when "string"
88
+ default_value.inspect
89
+ else
90
+ default_value
91
+ end
92
+ end
77
93
  end
78
94
  end
79
95
  end
@@ -3,13 +3,11 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Rails
6
- class IndexingEnhancement
6
+ class IndexingEnhancement < RubyIndexer::Enhancement
7
7
  extend T::Sig
8
- include RubyIndexer::Enhancement
9
8
 
10
9
  sig do
11
10
  override.params(
12
- index: RubyIndexer::Index,
13
11
  owner: T.nilable(RubyIndexer::Entry::Namespace),
14
12
  node: Prism::CallNode,
15
13
  file_path: String,
@@ -19,16 +17,16 @@ module RubyLsp
19
17
  ),
20
18
  ).void
21
19
  end
22
- def on_call_node(index, owner, node, file_path, code_units_cache)
20
+ def on_call_node_enter(owner, node, file_path, code_units_cache)
23
21
  return unless owner
24
22
 
25
23
  name = node.name
26
24
 
27
25
  case name
28
26
  when :extend
29
- handle_concern_extend(index, owner, node)
27
+ handle_concern_extend(owner, node)
30
28
  when :has_one, :has_many, :belongs_to, :has_and_belongs_to_many
31
- handle_association(index, owner, node, file_path, code_units_cache)
29
+ handle_association(owner, node, file_path, code_units_cache)
32
30
  end
33
31
  end
34
32
 
@@ -36,7 +34,6 @@ module RubyLsp
36
34
 
37
35
  sig do
38
36
  params(
39
- index: RubyIndexer::Index,
40
37
  owner: RubyIndexer::Entry::Namespace,
41
38
  node: Prism::CallNode,
42
39
  file_path: String,
@@ -46,7 +43,7 @@ module RubyLsp
46
43
  ),
47
44
  ).void
48
45
  end
49
- def handle_association(index, owner, node, file_path, code_units_cache)
46
+ def handle_association(owner, node, file_path, code_units_cache)
50
47
  arguments = node.arguments&.arguments
51
48
  return unless arguments
52
49
 
@@ -64,7 +61,7 @@ module RubyLsp
64
61
  loc = RubyIndexer::Location.from_prism_location(name_arg.location, code_units_cache)
65
62
 
66
63
  # Reader
67
- index.add(RubyIndexer::Entry::Method.new(
64
+ @index.add(RubyIndexer::Entry::Method.new(
68
65
  name,
69
66
  file_path,
70
67
  loc,
@@ -76,7 +73,7 @@ module RubyLsp
76
73
  ))
77
74
 
78
75
  # Writer
79
- index.add(RubyIndexer::Entry::Method.new(
76
+ @index.add(RubyIndexer::Entry::Method.new(
80
77
  "#{name}=",
81
78
  file_path,
82
79
  loc,
@@ -90,12 +87,11 @@ module RubyLsp
90
87
 
91
88
  sig do
92
89
  params(
93
- index: RubyIndexer::Index,
94
90
  owner: RubyIndexer::Entry::Namespace,
95
91
  node: Prism::CallNode,
96
92
  ).void
97
93
  end
98
- def handle_concern_extend(index, owner, node)
94
+ def handle_concern_extend(owner, node)
99
95
  arguments = node.arguments&.arguments
100
96
  return unless arguments
101
97
 
@@ -105,7 +101,7 @@ module RubyLsp
105
101
  module_name = node.full_name
106
102
  next unless module_name == "ActiveSupport::Concern"
107
103
 
108
- index.register_included_hook(owner.name) do |index, base|
104
+ @index.register_included_hook(owner.name) do |index, base|
109
105
  class_methods_name = "#{owner.name}::ClassMethods"
110
106
 
111
107
  if index.indexed?(class_methods_name)
@@ -46,8 +46,6 @@ module RubyLsp
46
46
  class IncompleteMessageError < StandardError; end
47
47
  class EmptyMessageError < StandardError; end
48
48
 
49
- MAX_RETRIES = 5
50
-
51
49
  extend T::Sig
52
50
 
53
51
  sig { returns(String) }
@@ -70,6 +68,8 @@ module RubyLsp
70
68
  # https://github.com/Shopify/ruby-lsp-rails/issues/348
71
69
  end
72
70
 
71
+ log_message("Ruby LSP Rails booting server")
72
+
73
73
  stdin, stdout, stderr, wait_thread = Bundler.with_original_env do
74
74
  Open3.popen3("bundle", "exec", "rails", "runner", "#{__dir__}/server.rb", "start")
75
75
  end
@@ -77,6 +77,9 @@ module RubyLsp
77
77
  @stdin = T.let(stdin, IO)
78
78
  @stdout = T.let(stdout, IO)
79
79
  @stderr = T.let(stderr, IO)
80
+ @stdin.sync = true
81
+ @stdout.sync = true
82
+ @stderr.sync = true
80
83
  @wait_thread = T.let(wait_thread, Process::Waiter)
81
84
 
82
85
  # We set binmode for Windows compatibility
@@ -84,18 +87,8 @@ module RubyLsp
84
87
  @stdout.binmode
85
88
  @stderr.binmode
86
89
 
87
- log_message("Ruby LSP Rails booting server")
88
- count = 0
89
-
90
- begin
91
- count += 1
92
- initialize_response = T.must(read_response)
93
- @rails_root = T.let(initialize_response[:root], String)
94
- rescue EmptyMessageError
95
- log_message("Ruby LSP Rails is retrying initialize (#{count})")
96
- retry if count < MAX_RETRIES
97
- end
98
-
90
+ initialize_response = T.must(read_response)
91
+ @rails_root = T.let(initialize_response[:root], String)
99
92
  log_message("Finished booting Ruby LSP Rails server")
100
93
 
101
94
  unless ENV["RAILS_ENV"] == "test"
@@ -194,6 +187,29 @@ module RubyLsp
194
187
  )
195
188
  end
196
189
 
190
+ sig { returns(T.nilable(String)) }
191
+ def pending_migrations_message
192
+ response = make_request("pending_migrations_message")
193
+ response[:pending_migrations_message] if response
194
+ rescue IncompleteMessageError
195
+ log_message(
196
+ "Ruby LSP Rails failed when checking for pending migrations",
197
+ type: RubyLsp::Constant::MessageType::ERROR,
198
+ )
199
+ nil
200
+ end
201
+
202
+ sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
203
+ def run_migrations
204
+ make_request("run_migrations")
205
+ rescue IncompleteMessageError
206
+ log_message(
207
+ "Ruby LSP Rails failed to run migrations",
208
+ type: RubyLsp::Constant::MessageType::ERROR,
209
+ )
210
+ nil
211
+ end
212
+
197
213
  # Delegates a request to a server add-on
198
214
  sig do
199
215
  params(
@@ -274,11 +290,9 @@ module RubyLsp
274
290
  sig { overridable.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
275
291
  def read_response
276
292
  raw_response = @mutex.synchronize do
277
- headers = @stdout.gets("\r\n\r\n")
278
- raise IncompleteMessageError unless headers
279
-
280
- content_length = headers[/Content-Length: (\d+)/i, 1].to_i
281
- raise EmptyMessageError if content_length.zero?
293
+ content_length = read_content_length
294
+ content_length = read_content_length unless content_length
295
+ raise EmptyMessageError unless content_length
282
296
 
283
297
  @stdout.read(content_length)
284
298
  end
@@ -311,6 +325,17 @@ module RubyLsp
311
325
 
312
326
  @outgoing_queue << RubyLsp::Notification.window_log_message(message, type: type)
313
327
  end
328
+
329
+ sig { returns(T.nilable(Integer)) }
330
+ def read_content_length
331
+ headers = @stdout.gets("\r\n\r\n")
332
+ return unless headers
333
+
334
+ length = headers[/Content-Length: (\d+)/i, 1]
335
+ return unless length
336
+
337
+ length.to_i
338
+ end
314
339
  end
315
340
 
316
341
  class NullClient < RunnerClient
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "json"
5
+ require "open3"
5
6
 
6
7
  # NOTE: We should avoid printing to stderr since it causes problems. We never read the standard error pipe from the
7
8
  # client, so it will become full and eventually hang or crash. Instead, return a response with an `error` key.
@@ -109,6 +110,10 @@ module RubyLsp
109
110
  send_message(resolve_database_info_from_model(params.fetch(:name)))
110
111
  when "association_target_location"
111
112
  send_message(resolve_association_target(params))
113
+ when "pending_migrations_message"
114
+ send_message({ result: { pending_migrations_message: pending_migrations_message } })
115
+ when "run_migrations"
116
+ send_message({ result: run_migrations })
112
117
  when "reload"
113
118
  ::Rails.application.reloader.reload!
114
119
  when "route_location"
@@ -167,6 +172,11 @@ module RubyLsp
167
172
  if ActionDispatch::Routing::Mapper.respond_to?(:route_source_locations) &&
168
173
  ActionDispatch::Routing::Mapper.route_source_locations
169
174
  def route_location(name)
175
+ # In Rails 8, Rails.application.routes.named_routes is not populated by default
176
+ if ::Rails.application.respond_to?(:reload_routes_unless_loaded)
177
+ ::Rails.application.reload_routes_unless_loaded
178
+ end
179
+
170
180
  match_data = name.match(/^(.+)(_path|_url)$/)
171
181
  return { result: nil } unless match_data
172
182
 
@@ -200,7 +210,7 @@ module RubyLsp
200
210
 
201
211
  info = {
202
212
  result: {
203
- columns: const.columns.map { |column| [column.name, column.type] },
213
+ columns: const.columns.map { |column| [column.name, column.type, column.default, column.null] },
204
214
  primary_keys: Array(const.primary_key),
205
215
  },
206
216
  }
@@ -247,6 +257,26 @@ module RubyLsp
247
257
  !const.abstract_class?
248
258
  )
249
259
  end
260
+
261
+ def pending_migrations_message
262
+ return unless defined?(ActiveRecord)
263
+
264
+ ActiveRecord::Migration.check_all_pending!
265
+ nil
266
+ rescue ActiveRecord::PendingMigrationError => e
267
+ e.message
268
+ end
269
+
270
+ def run_migrations
271
+ # Running migrations invokes `load` which will repeatedly load the same files. It's not designed to be invoked
272
+ # multiple times within the same process. To avoid any memory bloat, we run migrations in a separate process
273
+ stdout, status = Open3.capture2(
274
+ { "VERBOSE" => "true" },
275
+ "bundle exec rails db:migrate",
276
+ )
277
+
278
+ { message: stdout, status: status.exitstatus }
279
+ end
250
280
  end
251
281
  end
252
282
  end
@@ -3,6 +3,6 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Rails
6
- VERSION = "0.3.21"
6
+ VERSION = "0.3.22"
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.21
4
+ version: 0.3.22
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-23 00:00:00.000000000 Z
11
+ date: 2024-11-05 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.20.0
19
+ version: 0.21.2
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: 0.21.0
22
+ version: 0.22.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.20.0
29
+ version: 0.21.2
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: 0.21.0
32
+ version: 0.22.0
33
33
  description: A Ruby LSP addon that adds extra editor functionality for Rails applications
34
34
  email:
35
35
  - ruby@shopify.com