ruby-lsp-rails 0.3.21 → 0.3.23

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: feb1936ab1a1b20409425b3d644ab0e3c158b6f21a6bdf024d73013ccac28120
4
- data.tar.gz: f32daf2f444a0ca0fa2a55b0d2bdef2d67abfcf40b4bae5cd351eede9b50dc11
3
+ metadata.gz: 03f59a93f09d79b974b5a46176ef8d2745e361bce2247ca19bef22e72aa697f8
4
+ data.tar.gz: 7e65330c616d0db49d4d54f084284ac8fd9d181b271e68dce3f3408a66d81daf
5
5
  SHA512:
6
- metadata.gz: 72082ba3519fec8b3453201b9ffa9a270d745bdb4abff0e70edce58ad82024aae836a604bcdbdcec73f54faaae8b28e5a646ffec4bd0910c88099d425f3a6606
7
- data.tar.gz: 4c53919cae2b20d3dbd6402721ebe9470ba89ab4a2db24cbc61f04484d8b19c23c7c99017042ac09fff55e4f3cf34c2830890a270245b89011a0691ba6ab11f1
6
+ metadata.gz: 1d92c385c3e6b7e1615213434791295bc2e6488d6e3dd853946c2c1f59345a2e0dd44a1e3019ca46293b8f48423a656f6e756941fc4da5eb371fd084735b5bce
7
+ data.tar.gz: ac372a79150173f46c8ec6d72e686785f41b98d1d986502c360e91d5fb461e8b2a7bd4a348f617365fd42c4863b7c008e140d545c06639dbcb5946dd2ed33a10
@@ -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
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.23"
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.23
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