ruby-lsp-rails 0.3.2 → 0.3.3

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: 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