ruby-lsp-rails 0.3.2 → 0.3.3

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: 7b68f8a0b2e4d7d4fa32aa2712865dea5c4734c86aa6a490c2a7ace718e3405a
4
- data.tar.gz: a146d2ff4091073a9517147dffd3f5326337b947563ed39848d3a823662b5fdc
3
+ metadata.gz: 0c6faaac7ecd3824bd041d7273207db6c8611306ef0a33d7d02eb42cc8e931c0
4
+ data.tar.gz: 26ee8d231aab1512d52547056838f42a194265645fceb80dc9c02d77eba0e286
5
5
  SHA512:
6
- metadata.gz: 8902056649daff38b9c6e73231ecc0fb55485d4789d52b9dbf92afe4983104082a959b242b1a9f898d3697344663a678da952b0c0a4f76db026ef07c4744fe52
7
- data.tar.gz: 7550880a0a82da99734d38178c5aa47a1909b1787f2070bbf960f2b70ea177a3dd330484428b220de10d3d511a93b7f87a736dfbaf5c47bd12c50f547da15858
6
+ metadata.gz: e6d4711e5a31d9a59b4648e693796ab1398c04abe9b00e55627a6815b989751b8764d0c33a26679a4530aeadf8900d0f7d0f59b0dd0ab55fb68365be831f2199
7
+ data.tar.gz: e55de08bde09f2b065ec8820ac29f831ec094efdb23eb8ce930f488c851ad3b4e183ef7f0dafca5d16d511ffa87ae0be20bba4c903a0d4b6d9b2dee2532df369
data/README.md CHANGED
@@ -7,37 +7,26 @@ Ruby LSP Rails is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) addon for ex
7
7
 
8
8
  ## Installation
9
9
 
10
- To install, add the following line to your application's Gemfile:
10
+ If you haven't already done so, you'll need to first [set up Ruby LSP](https://github.com/Shopify/ruby-lsp#usage).
11
11
 
12
- ```ruby
13
- # Gemfile
14
- group :development do
15
- gem "ruby-lsp-rails"
16
- end
17
- ```
12
+ 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.
13
+ There is no need to add the gem to your bundle.
18
14
 
19
- ## Usage
15
+ ## Features
20
16
 
21
- ### Hover to reveal ActiveRecord schema
17
+ * Hover over an ActiveRecord model to reveal its schema.
18
+ * Run or debug a test by clicking on the code lens which appears above the test class, or an individual test.
19
+ * Navigate to validations, callbacks and test cases using your editor's "Go to Symbol" feature, or outline view.
22
20
 
23
- 1. Start your Rails server
24
- 1. Hover over an ActiveRecord model to see its details
25
-
26
- ### Documentation
21
+ ## Documentation
27
22
 
28
23
  See the [documentation](https://shopify.github.io/ruby-lsp-rails) for more in-depth details about the
29
24
  [supported features](https://shopify.github.io/ruby-lsp-rails/RubyLsp/Rails.html).
30
25
 
31
- ### Running Tests
32
-
33
- 1. Open a test which inherits from `ActiveSupport::TestCase` or one of its descendants, such as `ActionDispatch::IntegrationTest`.
34
- 2. Click on the "Run", "Run in Terminal" or "Debug" code lens which appears above the test class, or an individual test.
35
-
36
- > [!NOTE]
37
- > When using the Test Explorer view, if your code contains a statement to pause execution (e.g. `debugger`) it will
38
- > cause the test runner to hang.
26
+ ## How Runtime Introspection Works
39
27
 
40
- ## How It Works
28
+ LSP tooling is typically based on static analysis, but `ruby-lsp-rails` actually communicates with your Rails app for
29
+ some features.
41
30
 
42
31
  When Ruby LSP Rails starts, it spawns a `rails runner` instance which runs
43
32
  [`server.rb`](https://github.com/Shopify/ruby-lsp-rails/blob/main/lib/ruby_lsp/ruby_lsp_rails/server.rb).
@@ -46,7 +35,7 @@ The addon communicates with this process over a pipe (i.e. `stdin` and `stdout`)
46
35
  When extension is stopped (e.g. by quitting the editor), the server instance is shut down.
47
36
 
48
37
  > [!NOTE]
49
- > Prior to v0.3, `ruby-lsp-rails` used a different approach which involved mounting a Rack application within the Rails app.
38
+ > Prior to v0.3.0, `ruby-lsp-rails` used a different approach which involved mounting a Rack application within the Rails app.
50
39
  > That approach was brittle and susceptible to the application's configuration, such as routing and middleware.
51
40
 
52
41
  ## Contributing
@@ -9,6 +9,7 @@ module RubyLsp
9
9
  #
10
10
  # - [Hover](rdoc-ref:RubyLsp::Rails::Hover)
11
11
  # - [CodeLens](rdoc-ref:RubyLsp::Rails::CodeLens)
12
+ # - [DocumentSymbol](rdoc-ref:RubyLsp::Rails::DocumentSymbol)
12
13
  module Rails
13
14
  end
14
15
  end
@@ -68,6 +68,15 @@ module RubyLsp
68
68
  DocumentSymbol.new(response_builder, dispatcher)
69
69
  end
70
70
 
71
+ sig { params(changes: T::Array[{ uri: String, type: Integer }]).void }
72
+ def workspace_did_change_watched_files(changes)
73
+ if changes.any? do |change|
74
+ change[:uri].end_with?("db/schema.rb") || change[:uri].end_with?("structure.sql")
75
+ end
76
+ @client.trigger_reload
77
+ end
78
+ end
79
+
71
80
  sig { override.returns(String) }
72
81
  def name
73
82
  "Ruby LSP Rails"
@@ -12,7 +12,9 @@ module RubyLsp
12
12
  #
13
13
  # The
14
14
  # [code lens](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens)
15
- # request informs the editor of runnable commands such as tests
15
+ # request informs the editor of runnable commands such as tests.
16
+ # It's available for tests which inherit from `ActiveSupport::TestCase` or one of its descendants, such as
17
+ # `ActionDispatch::IntegrationTest`.
16
18
  #
17
19
  # # Example:
18
20
  #
@@ -32,6 +34,9 @@ module RubyLsp
32
34
  # ````
33
35
  #
34
36
  # The code lenses will be displayed above the class and above each test method.
37
+ #
38
+ # Note: When using the Test Explorer view, if your code contains a statement to pause execution (e.g. `debugger`) it
39
+ # will cause the test runner to hang.
35
40
  class CodeLens
36
41
  extend T::Sig
37
42
  include Requests::Support::Common
@@ -12,6 +12,63 @@ module RubyLsp
12
12
  include Requests::Support::Common
13
13
  include ActiveSupportTestCaseHelper
14
14
 
15
+ MODEL_CALLBACKS = T.let(
16
+ [
17
+ "before_validation",
18
+ "after_validation",
19
+ "before_save",
20
+ "around_save",
21
+ "after_save",
22
+ "before_create",
23
+ "around_create",
24
+ "after_create",
25
+ "after_commit",
26
+ "after_rollback",
27
+ "before_update",
28
+ "around_update",
29
+ "after_update",
30
+ "before_destroy",
31
+ "around_destroy",
32
+ "after_destroy",
33
+ "after_initialize",
34
+ "after_find",
35
+ "after_touch",
36
+ ].freeze,
37
+ T::Array[String],
38
+ )
39
+
40
+ CONTROLLER_CALLBACKS = T.let(
41
+ [
42
+ "after_action",
43
+ "append_after_action",
44
+ "append_around_action",
45
+ "append_before_action",
46
+ "around_action",
47
+ "before_action",
48
+ "prepend_after_action",
49
+ "prepend_around_action",
50
+ "prepend_before_action",
51
+ "skip_after_action",
52
+ "skip_around_action",
53
+ "skip_before_action",
54
+ ].freeze,
55
+ T::Array[String],
56
+ )
57
+
58
+ JOB_CALLBACKS = T.let(
59
+ [
60
+ "after_enqueue",
61
+ "after_perform",
62
+ "around_enqueue",
63
+ "around_perform",
64
+ "before_enqueue",
65
+ "before_perform",
66
+ ].freeze,
67
+ T::Array[String],
68
+ )
69
+
70
+ CALLBACKS = T.let((MODEL_CALLBACKS + CONTROLLER_CALLBACKS + JOB_CALLBACKS).freeze, T::Array[String])
71
+
15
72
  sig do
16
73
  params(
17
74
  response_builder: ResponseBuilders::DocumentSymbol,
@@ -28,13 +85,161 @@ module RubyLsp
28
85
  def on_call_node_enter(node)
29
86
  content = extract_test_case_name(node)
30
87
 
31
- return unless content
88
+ if content
89
+ append_document_symbol(
90
+ name: content,
91
+ selection_range: range_from_node(node),
92
+ range: range_from_node(node),
93
+ )
94
+ end
95
+
96
+ receiver = node.receiver
97
+ return if receiver && !receiver.is_a?(Prism::SelfNode)
98
+
99
+ message = node.message
100
+ case message
101
+ when *CALLBACKS, "validate"
102
+ handle_all_arg_types(node, T.must(message))
103
+ when "validates", "validates!", "validates_each"
104
+ handle_symbol_and_string_arg_types(node, T.must(message))
105
+ when "validates_with"
106
+ handle_class_arg_types(node, T.must(message))
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ sig { params(node: Prism::CallNode, message: String).void }
113
+ def handle_all_arg_types(node, message)
114
+ block = node.block
115
+
116
+ if block
117
+ append_document_symbol(
118
+ name: "#{message}(<anonymous>)",
119
+ range: range_from_location(node.location),
120
+ selection_range: range_from_location(block.location),
121
+ )
122
+ return
123
+ end
124
+
125
+ arguments = node.arguments&.arguments
126
+ return unless arguments&.any?
127
+
128
+ arguments.each do |argument|
129
+ case argument
130
+ when Prism::SymbolNode
131
+ name = argument.value
132
+ next unless name
133
+
134
+ append_document_symbol(
135
+ name: "#{message}(#{name})",
136
+ range: range_from_location(argument.location),
137
+ selection_range: range_from_location(T.must(argument.value_loc)),
138
+ )
139
+ when Prism::StringNode
140
+ name = argument.content
141
+ next if name.empty?
142
+
143
+ append_document_symbol(
144
+ name: "#{message}(#{name})",
145
+ range: range_from_location(argument.location),
146
+ selection_range: range_from_location(argument.content_loc),
147
+ )
148
+ when Prism::LambdaNode
149
+ append_document_symbol(
150
+ name: "#{message}(<anonymous>)",
151
+ range: range_from_location(node.location),
152
+ selection_range: range_from_location(argument.location),
153
+ )
154
+ when Prism::CallNode
155
+ next unless argument.name == :new
156
+
157
+ arg_receiver = argument.receiver
158
+
159
+ name = arg_receiver.name if arg_receiver.is_a?(Prism::ConstantReadNode)
160
+ name = arg_receiver.full_name if arg_receiver.is_a?(Prism::ConstantPathNode)
161
+ next unless name
32
162
 
163
+ append_document_symbol(
164
+ name: "#{message}(#{name})",
165
+ range: range_from_location(argument.location),
166
+ selection_range: range_from_location(argument.location),
167
+ )
168
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
169
+ name = argument.full_name
170
+ next if name.empty?
171
+
172
+ append_document_symbol(
173
+ name: "#{message}(#{name})",
174
+ range: range_from_location(argument.location),
175
+ selection_range: range_from_location(argument.location),
176
+ )
177
+ end
178
+ end
179
+ end
180
+
181
+ sig { params(node: Prism::CallNode, message: String).void }
182
+ def handle_symbol_and_string_arg_types(node, message)
183
+ arguments = node.arguments&.arguments
184
+ return unless arguments&.any?
185
+
186
+ arguments.each do |argument|
187
+ case argument
188
+ when Prism::SymbolNode
189
+ name = argument.value
190
+ next unless name
191
+
192
+ append_document_symbol(
193
+ name: "#{message}(#{name})",
194
+ range: range_from_location(argument.location),
195
+ selection_range: range_from_location(T.must(argument.value_loc)),
196
+ )
197
+ when Prism::StringNode
198
+ name = argument.content
199
+ next if name.empty?
200
+
201
+ append_document_symbol(
202
+ name: "#{message}(#{name})",
203
+ range: range_from_location(argument.location),
204
+ selection_range: range_from_location(argument.content_loc),
205
+ )
206
+ end
207
+ end
208
+ end
209
+
210
+ sig { params(node: Prism::CallNode, message: String).void }
211
+ def handle_class_arg_types(node, message)
212
+ arguments = node.arguments&.arguments
213
+ return unless arguments&.any?
214
+
215
+ arguments.each do |argument|
216
+ case argument
217
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
218
+ name = argument.full_name
219
+ next if name.empty?
220
+
221
+ append_document_symbol(
222
+ name: "#{message}(#{name})",
223
+ range: range_from_location(argument.location),
224
+ selection_range: range_from_location(argument.location),
225
+ )
226
+ end
227
+ end
228
+ end
229
+
230
+ sig do
231
+ params(
232
+ name: String,
233
+ range: RubyLsp::Interface::Range,
234
+ selection_range: RubyLsp::Interface::Range,
235
+ ).void
236
+ end
237
+ def append_document_symbol(name:, range:, selection_range:)
33
238
  @response_builder.last.children << RubyLsp::Interface::DocumentSymbol.new(
34
- name: content,
35
- kind: LanguageServer::Protocol::Constant::SymbolKind::METHOD,
36
- selection_range: range_from_node(node),
37
- range: range_from_node(node),
239
+ name: name,
240
+ kind: RubyLsp::Constant::SymbolKind::METHOD,
241
+ range: range,
242
+ selection_range: selection_range,
38
243
  )
39
244
  end
40
245
  end
@@ -12,10 +12,18 @@ module RubyLsp
12
12
 
13
13
  sig { returns(RunnerClient) }
14
14
  def create_client
15
- new
15
+ if File.exist?("bin/rails")
16
+ new
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")
22
+ NullClient.new
23
+ end
16
24
  rescue Errno::ENOENT, StandardError => e # rubocop:disable Lint/ShadowedException
17
- warn("Ruby LSP Rails failed to initialize server: #{e.message}\n#{e.backtrace&.join("\n")}")
18
- warn("Server dependent features will not be available")
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")
19
27
  NullClient.new
20
28
  end
21
29
  end
@@ -50,14 +58,14 @@ module RubyLsp
50
58
  @stdin.binmode # for Windows compatibility
51
59
  @stdout.binmode # for Windows compatibility
52
60
 
53
- warn("Ruby LSP Rails booting server")
61
+ $stderr.puts("Ruby LSP Rails booting server")
54
62
  read_response
55
- warn("Finished booting Ruby LSP Rails server")
63
+ $stderr.puts("Finished booting Ruby LSP Rails server")
56
64
 
57
65
  unless ENV["RAILS_ENV"] == "test"
58
66
  at_exit do
59
67
  if @wait_thread.alive?
60
- warn("Ruby LSP Rails is force killing the server")
68
+ $stderr.puts("Ruby LSP Rails is force killing the server")
61
69
  sleep(0.5) # give the server a bit of time if we already issued a shutdown notification
62
70
  Process.kill(T.must(Signal.list["TERM"]), @wait_thread.pid)
63
71
  end
@@ -71,13 +79,22 @@ module RubyLsp
71
79
  def model(name)
72
80
  make_request("model", name: name)
73
81
  rescue IncompleteMessageError
74
- warn("Ruby LSP Rails failed to get model information: #{@stderr.read}")
82
+ $stderr.puts("Ruby LSP Rails failed to get model information: #{@stderr.read}")
83
+ nil
84
+ end
85
+
86
+ sig { void }
87
+ def trigger_reload
88
+ $stderr.puts("Reloading Rails application")
89
+ send_notification("reload")
90
+ rescue IncompleteMessageError
91
+ $stderr.puts("Ruby LSP Rails failed to trigger reload")
75
92
  nil
76
93
  end
77
94
 
78
95
  sig { void }
79
96
  def shutdown
80
- warn("Ruby LSP Rails shutting down server")
97
+ $stderr.puts("Ruby LSP Rails shutting down server")
81
98
  send_message("shutdown")
82
99
  sleep(0.5) # give the server a bit of time to shutdown
83
100
  [@stdin, @stdout, @stderr].each(&:close)
@@ -121,7 +138,7 @@ module RubyLsp
121
138
  response = JSON.parse(T.must(raw_response), symbolize_names: true)
122
139
 
123
140
  if response[:error]
124
- warn("Ruby LSP Rails error: " + response[:error])
141
+ $stderr.puts("Ruby LSP Rails error: " + response[:error])
125
142
  return
126
143
  end
127
144
 
@@ -57,16 +57,19 @@ module RubyLsp
57
57
  sig do
58
58
  params(
59
59
  request: String,
60
- params: T::Hash[Symbol, T.untyped],
60
+ params: T.nilable(T::Hash[Symbol, T.untyped]),
61
61
  ).returns(T.any(Object, T::Hash[Symbol, T.untyped]))
62
62
  end
63
- def execute(request, params = {})
63
+ def execute(request, params)
64
64
  case request
65
65
  when "shutdown"
66
66
  @running = false
67
67
  VOID
68
68
  when "model"
69
- resolve_database_info_from_model(params.fetch(:name))
69
+ resolve_database_info_from_model(T.must(params).fetch(:name))
70
+ when "reload"
71
+ ::Rails.application.reloader.reload!
72
+ VOID
70
73
  else
71
74
  VOID
72
75
  end
@@ -11,8 +11,6 @@ module RubyLsp
11
11
  message_value = node.message
12
12
  return unless message_value == "test" || message_value == "it"
13
13
 
14
- return unless node.arguments
15
-
16
14
  arguments = node.arguments&.arguments
17
15
  return unless arguments&.any?
18
16
 
@@ -66,7 +66,7 @@ module RubyLsp
66
66
  private def build_search_index
67
67
  return unless RAILTIES_VERSION
68
68
 
69
- warn("Fetching Rails Documents...")
69
+ $stderr.puts("Fetching Rails Documents...")
70
70
 
71
71
  response = Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/js/search_index.js"))
72
72
 
@@ -79,13 +79,13 @@ module RubyLsp
79
79
  response = Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/js/search_index.js"))
80
80
  response.body if response.is_a?(Net::HTTPSuccess)
81
81
  else
82
- warn("Response failed: #{response.inspect}")
82
+ $stderr.puts("Response failed: #{response.inspect}")
83
83
  nil
84
84
  end
85
85
 
86
86
  process_search_index(body) if body
87
87
  rescue StandardError => e
88
- warn("Exception occurred when fetching Rails document index: #{e.inspect}")
88
+ $stderr.puts("Exception occurred when fetching Rails document index: #{e.inspect}")
89
89
  end
90
90
 
91
91
  sig { params(js: String).returns(T::Hash[String, T::Array[T::Hash[Symbol, String]]]) }
@@ -3,6 +3,6 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Rails
6
- VERSION = "0.3.2"
6
+ VERSION = "0.3.3"
7
7
  end
8
8
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  # desc "Explaining what the task does"
3
4
  # task :ruby_lsp_rails do
4
5
  # # Task goes here
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.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-29 00:00:00.000000000 Z
11
+ date: 2024-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack