ruby-lsp-rails 0.3.16 → 0.3.18
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/lib/ruby_lsp/ruby_lsp_rails/addon.rb +30 -10
- data/lib/ruby_lsp/ruby_lsp_rails/code_lens.rb +5 -1
- data/lib/ruby_lsp/ruby_lsp_rails/hover.rb +3 -26
- data/lib/ruby_lsp/ruby_lsp_rails/indexing_enhancement.rb +2 -0
- data/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb +129 -34
- data/lib/ruby_lsp/ruby_lsp_rails/server.rb +69 -12
- data/lib/ruby_lsp_rails/version.rb +1 -1
- metadata +8 -9
- data/lib/ruby_lsp/ruby_lsp_rails/support/rails_document_client.rb +0 -131
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 159bd26f9edd1f611d7f13cb5864e63d8ecc1c6180bbf5a426b9e40a98ce25ba
|
4
|
+
data.tar.gz: 5266ed45904e0d62f074684a3a943ac1ef66b7827c4177ee7631d5ae46118553
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab185650a7d5d24f1d7e7faf29cea5eabc99d905525ea840b2000003059393c60be8b79ef9f97ef3a78bf58ae695a5d5f19aa5bbecc9ea367d48090c9614538c
|
7
|
+
data.tar.gz: ef04db908af2f8ce9ee58a0059e51736f969f487b769a421d7df141cfc0518f31c2aa503eace5cc5ad24db36f56a1855ba55ffc4d5990f1eccc5d8cea1a4931c
|
data/README.md
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
[![Ruby DX Slack](https://img.shields.io/badge/Slack-Ruby%20DX-success?logo=slack)](https://join.slack.com/t/ruby-dx/shared_invite/zt-2c8zjlir6-uUDJl8oIwcen_FS_aA~b6Q)
|
2
2
|
|
3
|
-
# Rails
|
3
|
+
# Rails add-on
|
4
4
|
|
5
|
-
The Rails
|
5
|
+
The Rails add-on is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) [add-on](https://shopify.github.io/ruby-lsp/add-ons.html) for extra [Rails editor features](https://shopify.github.io/ruby-lsp/rails-add-on.html).
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
9
9
|
If you haven't already done so, you'll need to first [set up Ruby LSP](https://shopify.github.io/ruby-lsp/#usage).
|
10
10
|
|
11
|
-
As of v0.3.0, Ruby LSP will automatically include the Ruby LSP Rails
|
11
|
+
As of v0.3.0, Ruby LSP will automatically include the Ruby LSP Rails add-on in its custom bundle when a Rails app is detected.
|
12
12
|
There is no need to add the gem to your bundle.
|
13
13
|
|
14
14
|
## Documentation
|
15
15
|
|
16
|
-
See the [documentation](https://shopify.github.io/ruby-lsp/rails-
|
17
|
-
[supported features](https://shopify.github.io/ruby-lsp/rails-
|
16
|
+
See the [documentation](https://shopify.github.io/ruby-lsp/rails-add-on.html) for more in-depth details about the
|
17
|
+
[supported features](https://shopify.github.io/ruby-lsp/rails-add-on.html#features).
|
18
18
|
|
19
19
|
## Contributing
|
20
20
|
|
@@ -28,20 +28,35 @@ module RubyLsp
|
|
28
28
|
# the real client is initialized, features that depend on it will not be blocked by using the NullClient
|
29
29
|
@rails_runner_client = T.let(NullClient.new, RunnerClient)
|
30
30
|
@global_state = T.let(nil, T.nilable(GlobalState))
|
31
|
+
@outgoing_queue = T.let(nil, T.nilable(Thread::Queue))
|
32
|
+
@addon_mutex = T.let(Mutex.new, Mutex)
|
33
|
+
@client_mutex = T.let(Mutex.new, Mutex)
|
34
|
+
@client_mutex.lock
|
35
|
+
|
36
|
+
Thread.new do
|
37
|
+
@addon_mutex.synchronize do
|
38
|
+
# We need to ensure the Rails client is fully loaded before we activate the server addons
|
39
|
+
@client_mutex.synchronize { @rails_runner_client = RunnerClient.create_client(T.must(@outgoing_queue)) }
|
40
|
+
end
|
41
|
+
end
|
31
42
|
end
|
32
43
|
|
33
44
|
sig { returns(RunnerClient) }
|
34
|
-
|
45
|
+
def rails_runner_client
|
46
|
+
@addon_mutex.synchronize { @rails_runner_client }
|
47
|
+
end
|
35
48
|
|
36
|
-
sig { override.params(global_state: GlobalState,
|
37
|
-
def activate(global_state,
|
49
|
+
sig { override.params(global_state: GlobalState, outgoing_queue: Thread::Queue).void }
|
50
|
+
def activate(global_state, outgoing_queue)
|
38
51
|
@global_state = global_state
|
39
|
-
|
40
|
-
|
41
|
-
Thread.new { @rails_runner_client = RunnerClient.create_client }
|
42
|
-
register_additional_file_watchers(global_state: global_state, message_queue: message_queue)
|
52
|
+
@outgoing_queue = outgoing_queue
|
53
|
+
@outgoing_queue << Notification.window_log_message("Activating Ruby LSP Rails add-on v#{VERSION}")
|
43
54
|
|
55
|
+
register_additional_file_watchers(global_state: global_state, outgoing_queue: outgoing_queue)
|
44
56
|
@global_state.index.register_enhancement(IndexingEnhancement.new)
|
57
|
+
|
58
|
+
# Start booting the real client in a background thread. Until this completes, the client will be a NullClient
|
59
|
+
@client_mutex.unlock
|
45
60
|
end
|
46
61
|
|
47
62
|
sig { override.void }
|
@@ -49,6 +64,11 @@ module RubyLsp
|
|
49
64
|
@rails_runner_client.shutdown
|
50
65
|
end
|
51
66
|
|
67
|
+
sig { override.returns(String) }
|
68
|
+
def version
|
69
|
+
VERSION
|
70
|
+
end
|
71
|
+
|
52
72
|
# Creates a new CodeLens listener. This method is invoked on every CodeLens request
|
53
73
|
sig do
|
54
74
|
override.params(
|
@@ -106,11 +126,11 @@ module RubyLsp
|
|
106
126
|
end
|
107
127
|
end
|
108
128
|
|
109
|
-
sig { params(global_state: GlobalState,
|
110
|
-
def register_additional_file_watchers(global_state:,
|
129
|
+
sig { params(global_state: GlobalState, outgoing_queue: Thread::Queue).void }
|
130
|
+
def register_additional_file_watchers(global_state:, outgoing_queue:)
|
111
131
|
return unless global_state.supports_watching_files
|
112
132
|
|
113
|
-
|
133
|
+
outgoing_queue << Request.new(
|
114
134
|
id: "ruby-lsp-rails-file-watcher",
|
115
135
|
method: "client/registerCapability",
|
116
136
|
params: Interface::RegistrationParams.new(
|
@@ -198,9 +198,13 @@ module RubyLsp
|
|
198
198
|
.gsub("::", "/")
|
199
199
|
.downcase
|
200
200
|
|
201
|
-
view_uris = Dir.glob("#{@client.rails_root}/app/views/#{controller_name}/#{action_name}*").
|
201
|
+
view_uris = Dir.glob("#{@client.rails_root}/app/views/#{controller_name}/#{action_name}*").filter_map do |path|
|
202
|
+
# it's possible we could have a directory with the same name as the action, so we need to skip those
|
203
|
+
next if File.directory?(path)
|
204
|
+
|
202
205
|
URI::Generic.from_path(path: path).to_s
|
203
206
|
end
|
207
|
+
|
204
208
|
return if view_uris.empty?
|
205
209
|
|
206
210
|
@response_builder << create_code_lens(
|
@@ -1,8 +1,6 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require_relative "support/rails_document_client"
|
5
|
-
|
6
4
|
module RubyLsp
|
7
5
|
module Rails
|
8
6
|
# ![Hover demo](../../hover.gif)
|
@@ -34,7 +32,7 @@ module RubyLsp
|
|
34
32
|
@response_builder = response_builder
|
35
33
|
@nesting = T.let(node_context.nesting, T::Array[String])
|
36
34
|
@index = T.let(global_state.index, RubyIndexer::Index)
|
37
|
-
dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter
|
35
|
+
dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter)
|
38
36
|
end
|
39
37
|
|
40
38
|
sig { params(node: Prism::ConstantPathNode).void }
|
@@ -43,10 +41,7 @@ module RubyLsp
|
|
43
41
|
return unless entries
|
44
42
|
|
45
43
|
name = T.must(entries.first).name
|
46
|
-
|
47
44
|
generate_column_content(name)
|
48
|
-
|
49
|
-
generate_rails_document_link_hover(name, node.location)
|
50
45
|
end
|
51
46
|
|
52
47
|
sig { params(node: Prism::ConstantReadNode).void }
|
@@ -57,16 +52,6 @@ module RubyLsp
|
|
57
52
|
generate_column_content(T.must(entries.first).name)
|
58
53
|
end
|
59
54
|
|
60
|
-
sig { params(node: Prism::CallNode).void }
|
61
|
-
def on_call_node_enter(node)
|
62
|
-
message_value = node.message
|
63
|
-
message_loc = node.message_loc
|
64
|
-
|
65
|
-
return unless message_value && message_loc
|
66
|
-
|
67
|
-
generate_rails_document_link_hover(message_value, message_loc)
|
68
|
-
end
|
69
|
-
|
70
55
|
private
|
71
56
|
|
72
57
|
sig { params(name: String).void }
|
@@ -77,8 +62,8 @@ module RubyLsp
|
|
77
62
|
schema_file = model[:schema_file]
|
78
63
|
|
79
64
|
@response_builder.push(
|
80
|
-
"[Schema](#{URI::Generic.from_path(path: schema_file)})",
|
81
|
-
category: :
|
65
|
+
"[Schema](#{URI::Generic.from_path(path: schema_file)})\n",
|
66
|
+
category: :documentation,
|
82
67
|
) if schema_file
|
83
68
|
|
84
69
|
@response_builder.push(
|
@@ -89,14 +74,6 @@ module RubyLsp
|
|
89
74
|
category: :documentation,
|
90
75
|
)
|
91
76
|
end
|
92
|
-
|
93
|
-
sig { params(name: String, location: Prism::Location).void }
|
94
|
-
def generate_rails_document_link_hover(name, location)
|
95
|
-
urls = Support::RailsDocumentClient.generate_rails_document_urls(name)
|
96
|
-
return if urls.empty?
|
97
|
-
|
98
|
-
@response_builder.push(urls.join("\n\n"), category: :links)
|
99
|
-
end
|
100
77
|
end
|
101
78
|
end
|
102
79
|
end
|
@@ -60,6 +60,7 @@ module RubyLsp
|
|
60
60
|
name_arg.location,
|
61
61
|
name_arg.location,
|
62
62
|
nil,
|
63
|
+
index.configuration.encoding,
|
63
64
|
[RubyIndexer::Entry::Signature.new([])],
|
64
65
|
RubyIndexer::Entry::Visibility::PUBLIC,
|
65
66
|
owner,
|
@@ -72,6 +73,7 @@ module RubyLsp
|
|
72
73
|
name_arg.location,
|
73
74
|
name_arg.location,
|
74
75
|
nil,
|
76
|
+
index.configuration.encoding,
|
75
77
|
[RubyIndexer::Entry::Signature.new([RubyIndexer::Entry::RequiredParameter.new(name: name.to_sym)])],
|
76
78
|
RubyIndexer::Entry::Visibility::PUBLIC,
|
77
79
|
owner,
|
@@ -10,20 +10,34 @@ module RubyLsp
|
|
10
10
|
class << self
|
11
11
|
extend T::Sig
|
12
12
|
|
13
|
-
sig { returns(RunnerClient) }
|
14
|
-
def create_client
|
13
|
+
sig { params(outgoing_queue: Thread::Queue).returns(RunnerClient) }
|
14
|
+
def create_client(outgoing_queue)
|
15
15
|
if File.exist?("bin/rails")
|
16
|
-
new
|
16
|
+
new(outgoing_queue)
|
17
17
|
else
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
unless outgoing_queue.closed?
|
19
|
+
outgoing_queue << RubyLsp::Notification.window_log_message(
|
20
|
+
<<~MESSAGE.chomp,
|
21
|
+
Ruby LSP Rails failed to locate bin/rails in the current directory: #{Dir.pwd}
|
22
|
+
Server dependent features will not be available
|
23
|
+
MESSAGE
|
24
|
+
type: RubyLsp::Constant::MessageType::WARNING,
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
22
28
|
NullClient.new
|
23
29
|
end
|
24
30
|
rescue Errno::ENOENT, StandardError => e # rubocop:disable Lint/ShadowedException
|
25
|
-
|
26
|
-
|
31
|
+
unless outgoing_queue.closed?
|
32
|
+
outgoing_queue << RubyLsp::Notification.window_log_message(
|
33
|
+
<<~MESSAGE.chomp,
|
34
|
+
Ruby LSP Rails failed to initialize server: #{e.full_message}
|
35
|
+
Server dependent features will not be available
|
36
|
+
MESSAGE
|
37
|
+
type: Constant::MessageType::ERROR,
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
27
41
|
NullClient.new
|
28
42
|
end
|
29
43
|
end
|
@@ -39,8 +53,9 @@ module RubyLsp
|
|
39
53
|
sig { returns(String) }
|
40
54
|
attr_reader :rails_root
|
41
55
|
|
42
|
-
sig { void }
|
43
|
-
def initialize
|
56
|
+
sig { params(outgoing_queue: Thread::Queue).void }
|
57
|
+
def initialize(outgoing_queue)
|
58
|
+
@outgoing_queue = T.let(outgoing_queue, Thread::Queue)
|
44
59
|
@mutex = T.let(Mutex.new, Mutex)
|
45
60
|
# Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
|
46
61
|
# parent ends, the spring process ends as well. If this is not set, Spring will throw an error while trying to
|
@@ -69,7 +84,7 @@ module RubyLsp
|
|
69
84
|
@stdout.binmode
|
70
85
|
@stderr.binmode
|
71
86
|
|
72
|
-
|
87
|
+
log_message("Ruby LSP Rails booting server")
|
73
88
|
count = 0
|
74
89
|
|
75
90
|
begin
|
@@ -77,30 +92,52 @@ module RubyLsp
|
|
77
92
|
initialize_response = T.must(read_response)
|
78
93
|
@rails_root = T.let(initialize_response[:root], String)
|
79
94
|
rescue EmptyMessageError
|
80
|
-
|
95
|
+
log_message("Ruby LSP Rails is retrying initialize (#{count})")
|
81
96
|
retry if count < MAX_RETRIES
|
82
97
|
end
|
83
98
|
|
84
|
-
|
99
|
+
log_message("Finished booting Ruby LSP Rails server")
|
85
100
|
|
86
101
|
unless ENV["RAILS_ENV"] == "test"
|
87
102
|
at_exit do
|
88
103
|
if @wait_thread.alive?
|
89
|
-
$stderr.puts("Ruby LSP Rails is force killing the server")
|
90
104
|
sleep(0.5) # give the server a bit of time if we already issued a shutdown notification
|
91
105
|
force_kill
|
92
106
|
end
|
93
107
|
end
|
94
108
|
end
|
109
|
+
|
110
|
+
@logger_thread = T.let(
|
111
|
+
Thread.new do
|
112
|
+
while (content = @stderr.gets("\n"))
|
113
|
+
log_message(content, type: RubyLsp::Constant::MessageType::LOG)
|
114
|
+
end
|
115
|
+
end,
|
116
|
+
Thread,
|
117
|
+
)
|
95
118
|
rescue Errno::EPIPE, IncompleteMessageError
|
96
119
|
raise InitializationError, @stderr.read
|
97
120
|
end
|
98
121
|
|
122
|
+
sig { params(server_addon_path: String).void }
|
123
|
+
def register_server_addon(server_addon_path)
|
124
|
+
send_notification("server_addon/register", server_addon_path: server_addon_path)
|
125
|
+
rescue IncompleteMessageError
|
126
|
+
log_message(
|
127
|
+
"Ruby LSP Rails failed to register server addon #{server_addon_path}",
|
128
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
129
|
+
)
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
|
99
133
|
sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
100
134
|
def model(name)
|
101
135
|
make_request("model", name: name)
|
102
136
|
rescue IncompleteMessageError
|
103
|
-
|
137
|
+
log_message(
|
138
|
+
"Ruby LSP Rails failed to get model information",
|
139
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
140
|
+
)
|
104
141
|
nil
|
105
142
|
end
|
106
143
|
|
@@ -116,15 +153,22 @@ module RubyLsp
|
|
116
153
|
model_name: model_name,
|
117
154
|
association_name: association_name,
|
118
155
|
)
|
119
|
-
rescue
|
120
|
-
|
156
|
+
rescue IncompleteMessageError
|
157
|
+
log_message(
|
158
|
+
"Ruby LSP Rails failed to get association location",
|
159
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
160
|
+
)
|
161
|
+
nil
|
121
162
|
end
|
122
163
|
|
123
164
|
sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
124
165
|
def route_location(name)
|
125
166
|
make_request("route_location", name: name)
|
126
167
|
rescue IncompleteMessageError
|
127
|
-
|
168
|
+
log_message(
|
169
|
+
"Ruby LSP Rails failed to get route location",
|
170
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
171
|
+
)
|
128
172
|
nil
|
129
173
|
end
|
130
174
|
|
@@ -132,22 +176,58 @@ module RubyLsp
|
|
132
176
|
def route(controller:, action:)
|
133
177
|
make_request("route_info", controller: controller, action: action)
|
134
178
|
rescue IncompleteMessageError
|
135
|
-
|
179
|
+
log_message(
|
180
|
+
"Ruby LSP Rails failed to get route information",
|
181
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
182
|
+
)
|
183
|
+
nil
|
184
|
+
end
|
185
|
+
|
186
|
+
# Delegates a notification to a server add-on
|
187
|
+
sig { params(server_addon_name: String, request_name: String, params: T.untyped).void }
|
188
|
+
def delegate_notification(server_addon_name:, request_name:, **params)
|
189
|
+
send_notification(
|
190
|
+
"server_addon/delegate",
|
191
|
+
request_name: request_name,
|
192
|
+
server_addon_name: server_addon_name,
|
193
|
+
**params,
|
194
|
+
)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Delegates a request to a server add-on
|
198
|
+
sig do
|
199
|
+
params(
|
200
|
+
server_addon_name: String,
|
201
|
+
request_name: String,
|
202
|
+
params: T.untyped,
|
203
|
+
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
204
|
+
end
|
205
|
+
def delegate_request(server_addon_name:, request_name:, **params)
|
206
|
+
make_request(
|
207
|
+
"server_addon/delegate",
|
208
|
+
server_addon_name: server_addon_name,
|
209
|
+
request_name: request_name,
|
210
|
+
**params,
|
211
|
+
)
|
212
|
+
rescue IncompleteMessageError
|
136
213
|
nil
|
137
214
|
end
|
138
215
|
|
139
216
|
sig { void }
|
140
217
|
def trigger_reload
|
141
|
-
|
218
|
+
log_message("Reloading Rails application")
|
142
219
|
send_notification("reload")
|
143
220
|
rescue IncompleteMessageError
|
144
|
-
|
221
|
+
log_message(
|
222
|
+
"Ruby LSP Rails failed to trigger reload",
|
223
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
224
|
+
)
|
145
225
|
nil
|
146
226
|
end
|
147
227
|
|
148
228
|
sig { void }
|
149
229
|
def shutdown
|
150
|
-
|
230
|
+
log_message("Ruby LSP Rails shutting down server")
|
151
231
|
send_message("shutdown")
|
152
232
|
sleep(0.5) # give the server a bit of time to shutdown
|
153
233
|
[@stdin, @stdout, @stderr].each(&:close)
|
@@ -164,24 +244,24 @@ module RubyLsp
|
|
164
244
|
sig do
|
165
245
|
params(
|
166
246
|
request: String,
|
167
|
-
params: T.
|
247
|
+
params: T.untyped,
|
168
248
|
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
169
249
|
end
|
170
|
-
def make_request(request, params
|
171
|
-
send_message(request, params)
|
250
|
+
def make_request(request, **params)
|
251
|
+
send_message(request, **params)
|
172
252
|
read_response
|
173
253
|
end
|
174
254
|
|
175
255
|
# Notifications are like messages, but one-way, with no response sent back.
|
176
|
-
sig { params(request: String, params: T.
|
177
|
-
def send_notification(request, params
|
256
|
+
sig { params(request: String, params: T.untyped).void }
|
257
|
+
def send_notification(request, **params) = send_message(request, **params)
|
178
258
|
|
179
259
|
private
|
180
260
|
|
181
|
-
sig { overridable.params(request: String, params: T.
|
182
|
-
def send_message(request, params
|
261
|
+
sig { overridable.params(request: String, params: T.untyped).void }
|
262
|
+
def send_message(request, **params)
|
183
263
|
message = { method: request }
|
184
|
-
message[:params] = params
|
264
|
+
message[:params] = params
|
185
265
|
json = message.to_json
|
186
266
|
|
187
267
|
@mutex.synchronize do
|
@@ -206,7 +286,10 @@ module RubyLsp
|
|
206
286
|
response = JSON.parse(T.must(raw_response), symbolize_names: true)
|
207
287
|
|
208
288
|
if response[:error]
|
209
|
-
|
289
|
+
log_message(
|
290
|
+
"Ruby LSP Rails error: #{response[:error]}",
|
291
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
292
|
+
)
|
210
293
|
return
|
211
294
|
end
|
212
295
|
|
@@ -221,6 +304,13 @@ module RubyLsp
|
|
221
304
|
# Windows does not support the `TERM` signal, so we're forced to use `KILL` here
|
222
305
|
Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
|
223
306
|
end
|
307
|
+
|
308
|
+
sig { params(message: ::String, type: ::Integer).void }
|
309
|
+
def log_message(message, type: RubyLsp::Constant::MessageType::LOG)
|
310
|
+
return if @outgoing_queue.closed?
|
311
|
+
|
312
|
+
@outgoing_queue << RubyLsp::Notification.window_log_message(message, type: type)
|
313
|
+
end
|
224
314
|
end
|
225
315
|
|
226
316
|
class NullClient < RunnerClient
|
@@ -247,8 +337,13 @@ module RubyLsp
|
|
247
337
|
|
248
338
|
private
|
249
339
|
|
250
|
-
sig {
|
251
|
-
def
|
340
|
+
sig { params(message: ::String, type: ::Integer).void }
|
341
|
+
def log_message(message, type: RubyLsp::Constant::MessageType::LOG)
|
342
|
+
# no-op
|
343
|
+
end
|
344
|
+
|
345
|
+
sig { override.params(request: String, params: T.untyped).void }
|
346
|
+
def send_message(request, **params)
|
252
347
|
# no-op
|
253
348
|
end
|
254
349
|
|
@@ -8,7 +8,63 @@ require "json"
|
|
8
8
|
|
9
9
|
module RubyLsp
|
10
10
|
module Rails
|
11
|
+
module Common
|
12
|
+
# Write a message to the client. Can be used for sending notifications to the editor
|
13
|
+
def send_message(message)
|
14
|
+
json_message = message.to_json
|
15
|
+
@stdout.write("Content-Length: #{json_message.length}\r\n\r\n#{json_message}")
|
16
|
+
end
|
17
|
+
|
18
|
+
# Log a debug message to the editor's output
|
19
|
+
def debug_message(message)
|
20
|
+
$stderr.puts(message)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ServerAddon
|
25
|
+
include Common
|
26
|
+
|
27
|
+
@server_addon_classes = []
|
28
|
+
@server_addons = {}
|
29
|
+
|
30
|
+
class << self
|
31
|
+
# We keep track of runtime server add-ons the same way we track other add-ons, by storing classes that inherit
|
32
|
+
# from the base one
|
33
|
+
def inherited(child)
|
34
|
+
@server_addon_classes << child
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
# Delegate `request` with `params` to the server add-on with the given `name`
|
39
|
+
def delegate(name, request, params)
|
40
|
+
@server_addons[name]&.execute(request, params)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Instantiate all server addons and store them in a hash for easy access after we have discovered the classes
|
44
|
+
def finalize_registrations!(stdout)
|
45
|
+
until @server_addon_classes.empty?
|
46
|
+
addon = @server_addon_classes.shift.new(stdout)
|
47
|
+
@server_addons[addon.name] = addon
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(stdout)
|
53
|
+
@stdout = stdout
|
54
|
+
end
|
55
|
+
|
56
|
+
def name
|
57
|
+
raise NotImplementedError, "Not implemented!"
|
58
|
+
end
|
59
|
+
|
60
|
+
def execute(request, params)
|
61
|
+
raise NotImplementedError, "Not implemented!"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
11
65
|
class Server
|
66
|
+
include Common
|
67
|
+
|
12
68
|
def initialize(stdout: $stdout, override_default_output_device: true)
|
13
69
|
# Grab references to the original pipes so that we can change the default output device further down
|
14
70
|
@stdin = $stdin
|
@@ -34,8 +90,7 @@ module RubyLsp
|
|
34
90
|
routes_reloader = ::Rails.application.routes_reloader
|
35
91
|
routes_reloader.execute_unless_loaded if routes_reloader&.respond_to?(:execute_unless_loaded)
|
36
92
|
|
37
|
-
|
38
|
-
@stdout.write("Content-Length: #{initialize_result.length}\r\n\r\n#{initialize_result}")
|
93
|
+
send_message({ result: { message: "ok", root: ::Rails.root.to_s } })
|
39
94
|
|
40
95
|
while @running
|
41
96
|
headers = @stdin.gets("\r\n\r\n")
|
@@ -51,27 +106,29 @@ module RubyLsp
|
|
51
106
|
when "shutdown"
|
52
107
|
@running = false
|
53
108
|
when "model"
|
54
|
-
|
109
|
+
send_message(resolve_database_info_from_model(params.fetch(:name)))
|
55
110
|
when "association_target_location"
|
56
|
-
|
111
|
+
send_message(resolve_association_target(params))
|
57
112
|
when "reload"
|
58
113
|
::Rails.application.reloader.reload!
|
59
114
|
when "route_location"
|
60
|
-
|
115
|
+
send_message(route_location(params.fetch(:name)))
|
61
116
|
when "route_info"
|
62
|
-
|
117
|
+
send_message(resolve_route_info(params))
|
118
|
+
when "server_addon/register"
|
119
|
+
require params[:server_addon_path]
|
120
|
+
ServerAddon.finalize_registrations!(@stdout)
|
121
|
+
when "server_addon/delegate"
|
122
|
+
server_addon_name = params.delete(:server_addon_name)
|
123
|
+
request_name = params.delete(:request_name)
|
124
|
+
ServerAddon.delegate(server_addon_name, request_name, params)
|
63
125
|
end
|
64
126
|
rescue => e
|
65
|
-
|
127
|
+
send_message({ error: e.full_message(highlight: false) })
|
66
128
|
end
|
67
129
|
|
68
130
|
private
|
69
131
|
|
70
|
-
def write_response(response)
|
71
|
-
json_response = response.to_json
|
72
|
-
@stdout.write("Content-Length: #{json_response.length}\r\n\r\n#{json_response}")
|
73
|
-
end
|
74
|
-
|
75
132
|
def resolve_route_info(requirements)
|
76
133
|
if requirements[:controller]
|
77
134
|
requirements[:controller] = requirements.fetch(:controller).underscore.delete_suffix("_controller")
|
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.18
|
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-10-07 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.
|
19
|
+
version: 0.19.0
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: 0.
|
22
|
+
version: 0.20.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.
|
29
|
+
version: 0.19.0
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: 0.
|
32
|
+
version: 0.20.0
|
33
33
|
description: A Ruby LSP addon that adds extra editor functionality for Rails applications
|
34
34
|
email:
|
35
35
|
- ruby@shopify.com
|
@@ -53,7 +53,6 @@ files:
|
|
53
53
|
- lib/ruby_lsp/ruby_lsp_rails/support/associations.rb
|
54
54
|
- lib/ruby_lsp/ruby_lsp_rails/support/callbacks.rb
|
55
55
|
- lib/ruby_lsp/ruby_lsp_rails/support/location_builder.rb
|
56
|
-
- lib/ruby_lsp/ruby_lsp_rails/support/rails_document_client.rb
|
57
56
|
- lib/ruby_lsp_rails/railtie.rb
|
58
57
|
- lib/ruby_lsp_rails/version.rb
|
59
58
|
- lib/tasks/ruby_lsp_rails_tasks.rake
|
@@ -65,7 +64,7 @@ metadata:
|
|
65
64
|
homepage_uri: https://github.com/Shopify/ruby-lsp-rails
|
66
65
|
source_code_uri: https://github.com/Shopify/ruby-lsp-rails
|
67
66
|
changelog_uri: https://github.com/Shopify/ruby-lsp-rails/releases
|
68
|
-
documentation_uri: https://shopify.github.io/ruby-lsp/rails-
|
67
|
+
documentation_uri: https://shopify.github.io/ruby-lsp/rails-add-on.html
|
69
68
|
post_install_message:
|
70
69
|
rdoc_options: []
|
71
70
|
require_paths:
|
@@ -81,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
80
|
- !ruby/object:Gem::Version
|
82
81
|
version: '0'
|
83
82
|
requirements: []
|
84
|
-
rubygems_version: 3.5.
|
83
|
+
rubygems_version: 3.5.20
|
85
84
|
signing_key:
|
86
85
|
specification_version: 4
|
87
86
|
summary: A Ruby LSP addon for Rails
|
@@ -1,131 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
require "net/http"
|
5
|
-
|
6
|
-
module RubyLsp
|
7
|
-
module Rails
|
8
|
-
module Support
|
9
|
-
class RailsDocumentClient
|
10
|
-
RAILS_DOC_HOST = "https://api.rubyonrails.org"
|
11
|
-
|
12
|
-
SUPPORTED_RAILS_DOC_NAMESPACES = T.let(
|
13
|
-
Regexp.union(
|
14
|
-
/ActionDispatch/,
|
15
|
-
/ActionController/,
|
16
|
-
/AbstractController/,
|
17
|
-
/ActiveRecord/,
|
18
|
-
/ActiveModel/,
|
19
|
-
/ActiveStorage/,
|
20
|
-
/ActionText/,
|
21
|
-
/ActiveJob/,
|
22
|
-
).freeze,
|
23
|
-
Regexp,
|
24
|
-
)
|
25
|
-
|
26
|
-
RAILTIES_VERSION = T.let(
|
27
|
-
[*::Gem::Specification.default_stubs, *::Gem::Specification.stubs].find do |s|
|
28
|
-
s.name == "railties"
|
29
|
-
end&.version&.to_s,
|
30
|
-
T.nilable(String),
|
31
|
-
)
|
32
|
-
|
33
|
-
class << self
|
34
|
-
extend T::Sig
|
35
|
-
|
36
|
-
sig { params(name: String).returns(T::Array[String]) }
|
37
|
-
def generate_rails_document_urls(name)
|
38
|
-
docs = search_index&.fetch(name, nil)
|
39
|
-
|
40
|
-
return [] unless docs
|
41
|
-
|
42
|
-
docs.map do |doc|
|
43
|
-
owner = doc[:owner]
|
44
|
-
|
45
|
-
link_name =
|
46
|
-
# class/module name
|
47
|
-
if owner == name
|
48
|
-
name
|
49
|
-
else
|
50
|
-
"#{owner}##{name}"
|
51
|
-
end
|
52
|
-
|
53
|
-
"[Rails Document: `#{link_name}`](#{doc[:url]})"
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
sig { returns(T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]])) }
|
58
|
-
private def search_index
|
59
|
-
@rails_documents ||= T.let(
|
60
|
-
build_search_index,
|
61
|
-
T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]]),
|
62
|
-
)
|
63
|
-
end
|
64
|
-
|
65
|
-
sig { returns(T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]])) }
|
66
|
-
private def build_search_index
|
67
|
-
return unless RAILTIES_VERSION
|
68
|
-
|
69
|
-
$stderr.puts("Fetching search index for Rails documentation")
|
70
|
-
|
71
|
-
response = Net::HTTP.get_response(
|
72
|
-
URI("#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/js/search_index.js"),
|
73
|
-
{ "User-Agent" => "ruby-lsp-rails/#{RubyLsp::Rails::VERSION}" },
|
74
|
-
)
|
75
|
-
|
76
|
-
body = case response
|
77
|
-
when Net::HTTPSuccess
|
78
|
-
$stderr.puts("Finished fetching search index for Rails documentation")
|
79
|
-
response.body
|
80
|
-
when Net::HTTPRedirection
|
81
|
-
# If the version's doc is not found, e.g. Rails main, it'll be redirected
|
82
|
-
# In this case, we just fetch the latest doc
|
83
|
-
response = Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/js/search_index.js"))
|
84
|
-
if response.is_a?(Net::HTTPSuccess)
|
85
|
-
$stderr.puts("Finished fetching search index for Rails documentation")
|
86
|
-
response.body
|
87
|
-
end
|
88
|
-
else
|
89
|
-
$stderr.puts("Response failed: #{response.inspect}")
|
90
|
-
nil
|
91
|
-
end
|
92
|
-
|
93
|
-
process_search_index(body) if body
|
94
|
-
rescue StandardError => e
|
95
|
-
$stderr.puts("Exception occurred when fetching Rails document index: #{e.inspect}")
|
96
|
-
end
|
97
|
-
|
98
|
-
sig { params(js: String).returns(T::Hash[String, T::Array[T::Hash[Symbol, String]]]) }
|
99
|
-
private def process_search_index(js)
|
100
|
-
raw_data = js.sub("var search_data = ", "")
|
101
|
-
info = JSON.parse(raw_data).dig("index", "info")
|
102
|
-
|
103
|
-
# An entry looks like this:
|
104
|
-
#
|
105
|
-
# ["belongs_to", # method or module/class
|
106
|
-
# "ActiveRecord::Associations::ClassMethods", # method owner
|
107
|
-
# "classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to", # path to the document
|
108
|
-
# "(name, scope = nil, **options)", # method's parameters
|
109
|
-
# "<p>Specifies a one-to-one association with another class..."] # document preview
|
110
|
-
#
|
111
|
-
info.each_with_object({}) do |(method_or_class, method_owner, doc_path, _, doc_preview), table|
|
112
|
-
# If a method doesn't have documentation, there's no need to generate the link to it.
|
113
|
-
next if doc_preview.nil? || doc_preview.empty?
|
114
|
-
|
115
|
-
# If the method or class/module is not from the supported namespace, reject it
|
116
|
-
next unless [method_or_class, method_owner].any? do |elem|
|
117
|
-
elem.match?(SUPPORTED_RAILS_DOC_NAMESPACES)
|
118
|
-
end
|
119
|
-
|
120
|
-
owner = method_owner.empty? ? method_or_class : method_owner
|
121
|
-
table[method_or_class] ||= []
|
122
|
-
# It's possible to have multiple modules defining the same method name. For example,
|
123
|
-
# both `ActiveRecord::FinderMethods` and `ActiveRecord::Associations::CollectionProxy` defines `#find`
|
124
|
-
table[method_or_class] << { owner: owner, url: "#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/#{doc_path}" }
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|