ruby-lsp-rails 0.3.16 → 0.3.18
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 +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
|
[](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
|
# 
|
@@ -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
|