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 +4 -4
- data/README.md +12 -23
- data/lib/ruby-lsp-rails.rb +1 -0
- data/lib/ruby_lsp/ruby_lsp_rails/addon.rb +9 -0
- data/lib/ruby_lsp/ruby_lsp_rails/code_lens.rb +6 -1
- data/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +210 -5
- data/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb +26 -9
- data/lib/ruby_lsp/ruby_lsp_rails/server.rb +6 -3
- data/lib/ruby_lsp/ruby_lsp_rails/support/active_support_test_case_helper.rb +0 -2
- data/lib/ruby_lsp/ruby_lsp_rails/support/rails_document_client.rb +3 -3
- data/lib/ruby_lsp_rails/version.rb +1 -1
- data/lib/tasks/ruby_lsp_rails_tasks.rake +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c6faaac7ecd3824bd041d7273207db6c8611306ef0a33d7d02eb42cc8e931c0
|
4
|
+
data.tar.gz: 26ee8d231aab1512d52547056838f42a194265645fceb80dc9c02d77eba0e286
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
13
|
-
|
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
|
-
##
|
15
|
+
## Features
|
20
16
|
|
21
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/ruby-lsp-rails.rb
CHANGED
@@ -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
|
-
|
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:
|
35
|
-
kind:
|
36
|
-
|
37
|
-
|
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
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
61
|
+
$stderr.puts("Ruby LSP Rails booting server")
|
54
62
|
read_response
|
55
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -66,7 +66,7 @@ module RubyLsp
|
|
66
66
|
private def build_search_index
|
67
67
|
return unless RAILTIES_VERSION
|
68
68
|
|
69
|
-
|
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
|
-
|
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
|
-
|
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]]]) }
|
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.
|
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-
|
11
|
+
date: 2024-03-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|