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