ruby-lsp-rails 0.3.17 → 0.3.25
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/ruby_lsp/ruby_lsp_rails/addon.rb +108 -16
- data/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +3 -3
- data/lib/ruby_lsp/ruby_lsp_rails/hover.rb +19 -26
- data/lib/ruby_lsp/ruby_lsp_rails/indexing_enhancement.rb +23 -19
- data/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb +172 -59
- data/lib/ruby_lsp/ruby_lsp_rails/server.rb +67 -24
- data/lib/ruby_lsp_rails/version.rb +1 -1
- metadata +7 -8
- 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: 03b28222188faf1ac819337e73c244ba0a3cb588acfb286554246151b4beaa1a
|
4
|
+
data.tar.gz: 5558fe8142c90eaa53991f99a54da04b1cf1fe2ded213ae737b55cc9ad8acd78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb20529701e3008b9f38ba4840446aed72aad85b9e623bc93f691a7bee1ce0418d0b2bfabef085d5446fced0f645c0f6325dd4e78925f2e3ab20cc43e1d34543
|
7
|
+
data.tar.gz: e8507ef9f18e3987c667f1c45a3419171677f318bfb4ba5572153efea73bf2ec2922a0ec62865d3aabc9e49117af8e86951975a9eadc68d7848ce6d57f75fdb2
|
@@ -20,6 +20,8 @@ module RubyLsp
|
|
20
20
|
class Addon < ::RubyLsp::Addon
|
21
21
|
extend T::Sig
|
22
22
|
|
23
|
+
RUN_MIGRATIONS_TITLE = "Run Migrations"
|
24
|
+
|
23
25
|
sig { void }
|
24
26
|
def initialize
|
25
27
|
super
|
@@ -28,6 +30,7 @@ module RubyLsp
|
|
28
30
|
# the real client is initialized, features that depend on it will not be blocked by using the NullClient
|
29
31
|
@rails_runner_client = T.let(NullClient.new, RunnerClient)
|
30
32
|
@global_state = T.let(nil, T.nilable(GlobalState))
|
33
|
+
@outgoing_queue = T.let(nil, T.nilable(Thread::Queue))
|
31
34
|
@addon_mutex = T.let(Mutex.new, Mutex)
|
32
35
|
@client_mutex = T.let(Mutex.new, Mutex)
|
33
36
|
@client_mutex.lock
|
@@ -35,7 +38,8 @@ module RubyLsp
|
|
35
38
|
Thread.new do
|
36
39
|
@addon_mutex.synchronize do
|
37
40
|
# We need to ensure the Rails client is fully loaded before we activate the server addons
|
38
|
-
@client_mutex.synchronize { @rails_runner_client = RunnerClient.create_client }
|
41
|
+
@client_mutex.synchronize { @rails_runner_client = RunnerClient.create_client(T.must(@outgoing_queue)) }
|
42
|
+
offer_to_run_pending_migrations
|
39
43
|
end
|
40
44
|
end
|
41
45
|
end
|
@@ -45,12 +49,14 @@ module RubyLsp
|
|
45
49
|
@addon_mutex.synchronize { @rails_runner_client }
|
46
50
|
end
|
47
51
|
|
48
|
-
sig { override.params(global_state: GlobalState,
|
49
|
-
def activate(global_state,
|
52
|
+
sig { override.params(global_state: GlobalState, outgoing_queue: Thread::Queue).void }
|
53
|
+
def activate(global_state, outgoing_queue)
|
50
54
|
@global_state = global_state
|
51
|
-
|
52
|
-
|
53
|
-
|
55
|
+
@outgoing_queue = outgoing_queue
|
56
|
+
@outgoing_queue << Notification.window_log_message("Activating Ruby LSP Rails add-on v#{VERSION}")
|
57
|
+
|
58
|
+
register_additional_file_watchers(global_state: global_state, outgoing_queue: outgoing_queue)
|
59
|
+
@global_state.index.register_enhancement(IndexingEnhancement.new(@global_state.index))
|
54
60
|
|
55
61
|
# Start booting the real client in a background thread. Until this completes, the client will be a NullClient
|
56
62
|
@client_mutex.unlock
|
@@ -116,18 +122,87 @@ module RubyLsp
|
|
116
122
|
|
117
123
|
sig { params(changes: T::Array[{ uri: String, type: Integer }]).void }
|
118
124
|
def workspace_did_change_watched_files(changes)
|
119
|
-
if changes.any?
|
120
|
-
change[:uri].end_with?("db/schema.rb") || change[:uri].end_with?("structure.sql")
|
121
|
-
end
|
125
|
+
if changes.any? { |c| c[:uri].end_with?("db/schema.rb") || c[:uri].end_with?("structure.sql") }
|
122
126
|
@rails_runner_client.trigger_reload
|
123
127
|
end
|
128
|
+
|
129
|
+
if changes.any? do |c|
|
130
|
+
%r{db/migrate/.*\.rb}.match?(c[:uri]) && c[:type] != Constant::FileChangeType::CHANGED
|
131
|
+
end
|
132
|
+
|
133
|
+
offer_to_run_pending_migrations
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
sig { override.returns(String) }
|
138
|
+
def name
|
139
|
+
"Ruby LSP Rails"
|
140
|
+
end
|
141
|
+
|
142
|
+
sig { override.params(title: String).void }
|
143
|
+
def handle_window_show_message_response(title)
|
144
|
+
if title == RUN_MIGRATIONS_TITLE
|
145
|
+
|
146
|
+
begin_progress("run-migrations", "Running Migrations")
|
147
|
+
response = @rails_runner_client.run_migrations
|
148
|
+
|
149
|
+
if response && @outgoing_queue
|
150
|
+
if response[:status] == 0
|
151
|
+
# Both log the message and show it as part of progress because sometimes running migrations is so fast you
|
152
|
+
# can't see the progress notification
|
153
|
+
@outgoing_queue << Notification.window_log_message(response[:message])
|
154
|
+
report_progress("run-migrations", message: response[:message])
|
155
|
+
else
|
156
|
+
@outgoing_queue << Notification.window_show_message(
|
157
|
+
"Migrations failed to run\n\n#{response[:message]}",
|
158
|
+
type: Constant::MessageType::ERROR,
|
159
|
+
)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
end_progress("run-migrations")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
sig { params(id: String, title: String, percentage: T.nilable(Integer), message: T.nilable(String)).void }
|
170
|
+
def begin_progress(id, title, percentage: nil, message: nil)
|
171
|
+
return unless @global_state&.client_capabilities&.supports_progress && @outgoing_queue
|
172
|
+
|
173
|
+
@outgoing_queue << Request.new(
|
174
|
+
id: "progress-request-#{id}",
|
175
|
+
method: "window/workDoneProgress/create",
|
176
|
+
params: Interface::WorkDoneProgressCreateParams.new(token: id),
|
177
|
+
)
|
178
|
+
|
179
|
+
@outgoing_queue << Notification.progress_begin(
|
180
|
+
id,
|
181
|
+
title,
|
182
|
+
percentage: percentage,
|
183
|
+
message: "#{percentage}% completed",
|
184
|
+
)
|
185
|
+
end
|
186
|
+
|
187
|
+
sig { params(id: String, percentage: T.nilable(Integer), message: T.nilable(String)).void }
|
188
|
+
def report_progress(id, percentage: nil, message: nil)
|
189
|
+
return unless @global_state&.client_capabilities&.supports_progress && @outgoing_queue
|
190
|
+
|
191
|
+
@outgoing_queue << Notification.progress_report(id, percentage: percentage, message: message)
|
124
192
|
end
|
125
193
|
|
126
|
-
sig { params(
|
127
|
-
def
|
128
|
-
return unless global_state
|
194
|
+
sig { params(id: String).void }
|
195
|
+
def end_progress(id)
|
196
|
+
return unless @global_state&.client_capabilities&.supports_progress && @outgoing_queue
|
129
197
|
|
130
|
-
|
198
|
+
@outgoing_queue << Notification.progress_end(id)
|
199
|
+
end
|
200
|
+
|
201
|
+
sig { params(global_state: GlobalState, outgoing_queue: Thread::Queue).void }
|
202
|
+
def register_additional_file_watchers(global_state:, outgoing_queue:)
|
203
|
+
return unless global_state.client_capabilities.supports_watching_files
|
204
|
+
|
205
|
+
outgoing_queue << Request.new(
|
131
206
|
id: "ruby-lsp-rails-file-watcher",
|
132
207
|
method: "client/registerCapability",
|
133
208
|
params: Interface::RegistrationParams.new(
|
@@ -149,9 +224,26 @@ module RubyLsp
|
|
149
224
|
)
|
150
225
|
end
|
151
226
|
|
152
|
-
sig {
|
153
|
-
def
|
154
|
-
|
227
|
+
sig { void }
|
228
|
+
def offer_to_run_pending_migrations
|
229
|
+
return unless @outgoing_queue
|
230
|
+
return unless @global_state&.client_capabilities&.window_show_message_supports_extra_properties
|
231
|
+
|
232
|
+
migration_message = @rails_runner_client.pending_migrations_message
|
233
|
+
return unless migration_message
|
234
|
+
|
235
|
+
@outgoing_queue << Request.new(
|
236
|
+
id: "rails-pending-migrations",
|
237
|
+
method: "window/showMessageRequest",
|
238
|
+
params: {
|
239
|
+
type: Constant::MessageType::INFO,
|
240
|
+
message: migration_message,
|
241
|
+
actions: [
|
242
|
+
{ title: RUN_MIGRATIONS_TITLE, addon_name: name, method: "window/showMessageRequest" },
|
243
|
+
{ title: "Cancel", addon_name: name, method: "window/showMessageRequest" },
|
244
|
+
],
|
245
|
+
},
|
246
|
+
)
|
155
247
|
end
|
156
248
|
end
|
157
249
|
end
|
@@ -141,7 +141,7 @@ module RubyLsp
|
|
141
141
|
|
142
142
|
arg_receiver = argument.receiver
|
143
143
|
|
144
|
-
name = arg_receiver
|
144
|
+
name = constant_name(arg_receiver) if arg_receiver.is_a?(Prism::ConstantReadNode) ||
|
145
145
|
arg_receiver.is_a?(Prism::ConstantPathNode)
|
146
146
|
next unless name
|
147
147
|
|
@@ -201,8 +201,8 @@ module RubyLsp
|
|
201
201
|
arguments.each do |argument|
|
202
202
|
case argument
|
203
203
|
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
204
|
-
name = argument
|
205
|
-
next
|
204
|
+
name = constant_name(argument)
|
205
|
+
next unless name
|
206
206
|
|
207
207
|
append_document_symbol(
|
208
208
|
name: "#{message} #{name}",
|
@@ -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,25 +62,33 @@ 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(
|
85
|
-
model[:columns].map do |name, type|
|
70
|
+
model[:columns].map do |name, type, default_value, nullable|
|
86
71
|
primary_key_suffix = " (PK)" if model[:primary_keys].include?(name)
|
87
|
-
|
72
|
+
suffixes = []
|
73
|
+
suffixes << "default: #{format_default(default_value, type)}" if default_value
|
74
|
+
suffixes << "not null" unless nullable || primary_key_suffix
|
75
|
+
suffix_string = " - #{suffixes.join(" - ")}" if suffixes.any?
|
76
|
+
"**#{name}**: #{type}#{primary_key_suffix}#{suffix_string}\n"
|
88
77
|
end.join("\n"),
|
89
78
|
category: :documentation,
|
90
79
|
)
|
91
80
|
end
|
92
81
|
|
93
|
-
sig { params(
|
94
|
-
def
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
82
|
+
sig { params(default_value: String, type: String).returns(String) }
|
83
|
+
def format_default(default_value, type)
|
84
|
+
case type
|
85
|
+
when "boolean"
|
86
|
+
default_value == "true" ? "true" : "false"
|
87
|
+
when "string"
|
88
|
+
default_value.inspect
|
89
|
+
else
|
90
|
+
default_value
|
91
|
+
end
|
99
92
|
end
|
100
93
|
end
|
101
94
|
end
|
@@ -3,28 +3,30 @@
|
|
3
3
|
|
4
4
|
module RubyLsp
|
5
5
|
module Rails
|
6
|
-
class IndexingEnhancement
|
6
|
+
class IndexingEnhancement < RubyIndexer::Enhancement
|
7
7
|
extend T::Sig
|
8
|
-
include RubyIndexer::Enhancement
|
9
8
|
|
10
9
|
sig do
|
11
10
|
override.params(
|
12
|
-
index: RubyIndexer::Index,
|
13
11
|
owner: T.nilable(RubyIndexer::Entry::Namespace),
|
14
12
|
node: Prism::CallNode,
|
15
13
|
file_path: String,
|
14
|
+
code_units_cache: T.any(
|
15
|
+
T.proc.params(arg0: Integer).returns(Integer),
|
16
|
+
Prism::CodeUnitsCache,
|
17
|
+
),
|
16
18
|
).void
|
17
19
|
end
|
18
|
-
def
|
20
|
+
def on_call_node_enter(owner, node, file_path, code_units_cache)
|
19
21
|
return unless owner
|
20
22
|
|
21
23
|
name = node.name
|
22
24
|
|
23
25
|
case name
|
24
26
|
when :extend
|
25
|
-
handle_concern_extend(
|
27
|
+
handle_concern_extend(owner, node)
|
26
28
|
when :has_one, :has_many, :belongs_to, :has_and_belongs_to_many
|
27
|
-
handle_association(
|
29
|
+
handle_association(owner, node, file_path, code_units_cache)
|
28
30
|
end
|
29
31
|
end
|
30
32
|
|
@@ -32,13 +34,16 @@ module RubyLsp
|
|
32
34
|
|
33
35
|
sig do
|
34
36
|
params(
|
35
|
-
index: RubyIndexer::Index,
|
36
37
|
owner: RubyIndexer::Entry::Namespace,
|
37
38
|
node: Prism::CallNode,
|
38
39
|
file_path: String,
|
40
|
+
code_units_cache: T.any(
|
41
|
+
T.proc.params(arg0: Integer).returns(Integer),
|
42
|
+
Prism::CodeUnitsCache,
|
43
|
+
),
|
39
44
|
).void
|
40
45
|
end
|
41
|
-
def handle_association(
|
46
|
+
def handle_association(owner, node, file_path, code_units_cache)
|
42
47
|
arguments = node.arguments&.arguments
|
43
48
|
return unless arguments
|
44
49
|
|
@@ -53,27 +58,27 @@ module RubyLsp
|
|
53
58
|
|
54
59
|
return unless name
|
55
60
|
|
61
|
+
loc = RubyIndexer::Location.from_prism_location(name_arg.location, code_units_cache)
|
62
|
+
|
56
63
|
# Reader
|
57
|
-
index.add(RubyIndexer::Entry::Method.new(
|
64
|
+
@index.add(RubyIndexer::Entry::Method.new(
|
58
65
|
name,
|
59
66
|
file_path,
|
60
|
-
|
61
|
-
|
67
|
+
loc,
|
68
|
+
loc,
|
62
69
|
nil,
|
63
|
-
index.configuration.encoding,
|
64
70
|
[RubyIndexer::Entry::Signature.new([])],
|
65
71
|
RubyIndexer::Entry::Visibility::PUBLIC,
|
66
72
|
owner,
|
67
73
|
))
|
68
74
|
|
69
75
|
# Writer
|
70
|
-
index.add(RubyIndexer::Entry::Method.new(
|
76
|
+
@index.add(RubyIndexer::Entry::Method.new(
|
71
77
|
"#{name}=",
|
72
78
|
file_path,
|
73
|
-
|
74
|
-
|
79
|
+
loc,
|
80
|
+
loc,
|
75
81
|
nil,
|
76
|
-
index.configuration.encoding,
|
77
82
|
[RubyIndexer::Entry::Signature.new([RubyIndexer::Entry::RequiredParameter.new(name: name.to_sym)])],
|
78
83
|
RubyIndexer::Entry::Visibility::PUBLIC,
|
79
84
|
owner,
|
@@ -82,12 +87,11 @@ module RubyLsp
|
|
82
87
|
|
83
88
|
sig do
|
84
89
|
params(
|
85
|
-
index: RubyIndexer::Index,
|
86
90
|
owner: RubyIndexer::Entry::Namespace,
|
87
91
|
node: Prism::CallNode,
|
88
92
|
).void
|
89
93
|
end
|
90
|
-
def handle_concern_extend(
|
94
|
+
def handle_concern_extend(owner, node)
|
91
95
|
arguments = node.arguments&.arguments
|
92
96
|
return unless arguments
|
93
97
|
|
@@ -97,7 +101,7 @@ module RubyLsp
|
|
97
101
|
module_name = node.full_name
|
98
102
|
next unless module_name == "ActiveSupport::Concern"
|
99
103
|
|
100
|
-
index.register_included_hook(owner.name) do |index, base|
|
104
|
+
@index.register_included_hook(owner.name) do |index, base|
|
101
105
|
class_methods_name = "#{owner.name}::ClassMethods"
|
102
106
|
|
103
107
|
if index.indexed?(class_methods_name)
|
@@ -10,37 +10,51 @@ 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
|
30
44
|
|
31
45
|
class InitializationError < StandardError; end
|
32
|
-
class
|
33
|
-
class
|
34
|
-
|
35
|
-
MAX_RETRIES = 5
|
46
|
+
class MessageError < StandardError; end
|
47
|
+
class IncompleteMessageError < MessageError; end
|
48
|
+
class EmptyMessageError < MessageError; end
|
36
49
|
|
37
50
|
extend T::Sig
|
38
51
|
|
39
52
|
sig { returns(String) }
|
40
53
|
attr_reader :rails_root
|
41
54
|
|
42
|
-
sig { void }
|
43
|
-
def initialize
|
55
|
+
sig { params(outgoing_queue: Thread::Queue).void }
|
56
|
+
def initialize(outgoing_queue)
|
57
|
+
@outgoing_queue = T.let(outgoing_queue, Thread::Queue)
|
44
58
|
@mutex = T.let(Mutex.new, Mutex)
|
45
59
|
# Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
|
46
60
|
# parent ends, the spring process ends as well. If this is not set, Spring will throw an error while trying to
|
@@ -55,6 +69,8 @@ module RubyLsp
|
|
55
69
|
# https://github.com/Shopify/ruby-lsp-rails/issues/348
|
56
70
|
end
|
57
71
|
|
72
|
+
log_message("Ruby LSP Rails booting server")
|
73
|
+
|
58
74
|
stdin, stdout, stderr, wait_thread = Bundler.with_original_env do
|
59
75
|
Open3.popen3("bundle", "exec", "rails", "runner", "#{__dir__}/server.rb", "start")
|
60
76
|
end
|
@@ -62,6 +78,9 @@ module RubyLsp
|
|
62
78
|
@stdin = T.let(stdin, IO)
|
63
79
|
@stdout = T.let(stdout, IO)
|
64
80
|
@stderr = T.let(stderr, IO)
|
81
|
+
@stdin.sync = true
|
82
|
+
@stdout.sync = true
|
83
|
+
@stderr.sync = true
|
65
84
|
@wait_thread = T.let(wait_thread, Process::Waiter)
|
66
85
|
|
67
86
|
# We set binmode for Windows compatibility
|
@@ -69,29 +88,27 @@ module RubyLsp
|
|
69
88
|
@stdout.binmode
|
70
89
|
@stderr.binmode
|
71
90
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
begin
|
76
|
-
count += 1
|
77
|
-
initialize_response = T.must(read_response)
|
78
|
-
@rails_root = T.let(initialize_response[:root], String)
|
79
|
-
rescue EmptyMessageError
|
80
|
-
$stderr.puts("Ruby LSP Rails is retrying initialize (#{count})")
|
81
|
-
retry if count < MAX_RETRIES
|
82
|
-
end
|
83
|
-
|
84
|
-
$stderr.puts("Finished booting Ruby LSP Rails server")
|
91
|
+
initialize_response = T.must(read_response)
|
92
|
+
@rails_root = T.let(initialize_response[:root], String)
|
93
|
+
log_message("Finished booting Ruby LSP Rails server")
|
85
94
|
|
86
95
|
unless ENV["RAILS_ENV"] == "test"
|
87
96
|
at_exit do
|
88
97
|
if @wait_thread.alive?
|
89
|
-
$stderr.puts("Ruby LSP Rails is force killing the server")
|
90
98
|
sleep(0.5) # give the server a bit of time if we already issued a shutdown notification
|
91
99
|
force_kill
|
92
100
|
end
|
93
101
|
end
|
94
102
|
end
|
103
|
+
|
104
|
+
@logger_thread = T.let(
|
105
|
+
Thread.new do
|
106
|
+
while (content = @stderr.gets("\n"))
|
107
|
+
log_message(content, type: RubyLsp::Constant::MessageType::LOG)
|
108
|
+
end
|
109
|
+
end,
|
110
|
+
Thread,
|
111
|
+
)
|
95
112
|
rescue Errno::EPIPE, IncompleteMessageError
|
96
113
|
raise InitializationError, @stderr.read
|
97
114
|
end
|
@@ -99,16 +116,22 @@ module RubyLsp
|
|
99
116
|
sig { params(server_addon_path: String).void }
|
100
117
|
def register_server_addon(server_addon_path)
|
101
118
|
send_notification("server_addon/register", server_addon_path: server_addon_path)
|
102
|
-
rescue
|
103
|
-
|
119
|
+
rescue MessageError
|
120
|
+
log_message(
|
121
|
+
"Ruby LSP Rails failed to register server addon #{server_addon_path}",
|
122
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
123
|
+
)
|
104
124
|
nil
|
105
125
|
end
|
106
126
|
|
107
127
|
sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
108
128
|
def model(name)
|
109
129
|
make_request("model", name: name)
|
110
|
-
rescue
|
111
|
-
|
130
|
+
rescue MessageError
|
131
|
+
log_message(
|
132
|
+
"Ruby LSP Rails failed to get model information",
|
133
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
134
|
+
)
|
112
135
|
nil
|
113
136
|
end
|
114
137
|
|
@@ -124,38 +147,104 @@ module RubyLsp
|
|
124
147
|
model_name: model_name,
|
125
148
|
association_name: association_name,
|
126
149
|
)
|
127
|
-
rescue
|
128
|
-
|
150
|
+
rescue MessageError
|
151
|
+
log_message(
|
152
|
+
"Ruby LSP Rails failed to get association location",
|
153
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
154
|
+
)
|
155
|
+
nil
|
129
156
|
end
|
130
157
|
|
131
158
|
sig { params(name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
132
159
|
def route_location(name)
|
133
160
|
make_request("route_location", name: name)
|
134
|
-
rescue
|
135
|
-
|
161
|
+
rescue MessageError
|
162
|
+
log_message(
|
163
|
+
"Ruby LSP Rails failed to get route location",
|
164
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
165
|
+
)
|
136
166
|
nil
|
137
167
|
end
|
138
168
|
|
139
169
|
sig { params(controller: String, action: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
140
170
|
def route(controller:, action:)
|
141
171
|
make_request("route_info", controller: controller, action: action)
|
142
|
-
rescue
|
143
|
-
|
172
|
+
rescue MessageError
|
173
|
+
log_message(
|
174
|
+
"Ruby LSP Rails failed to get route information",
|
175
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
176
|
+
)
|
177
|
+
nil
|
178
|
+
end
|
179
|
+
|
180
|
+
# Delegates a notification to a server add-on
|
181
|
+
sig { params(server_addon_name: String, request_name: String, params: T.untyped).void }
|
182
|
+
def delegate_notification(server_addon_name:, request_name:, **params)
|
183
|
+
send_notification(
|
184
|
+
"server_addon/delegate",
|
185
|
+
request_name: request_name,
|
186
|
+
server_addon_name: server_addon_name,
|
187
|
+
**params,
|
188
|
+
)
|
189
|
+
end
|
190
|
+
|
191
|
+
sig { returns(T.nilable(String)) }
|
192
|
+
def pending_migrations_message
|
193
|
+
response = make_request("pending_migrations_message")
|
194
|
+
response[:pending_migrations_message] if response
|
195
|
+
rescue MessageError
|
196
|
+
log_message(
|
197
|
+
"Ruby LSP Rails failed when checking for pending migrations",
|
198
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
199
|
+
)
|
200
|
+
nil
|
201
|
+
end
|
202
|
+
|
203
|
+
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
204
|
+
def run_migrations
|
205
|
+
make_request("run_migrations")
|
206
|
+
rescue MessageError
|
207
|
+
log_message(
|
208
|
+
"Ruby LSP Rails failed to run migrations",
|
209
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
210
|
+
)
|
211
|
+
nil
|
212
|
+
end
|
213
|
+
|
214
|
+
# Delegates a request to a server add-on
|
215
|
+
sig do
|
216
|
+
params(
|
217
|
+
server_addon_name: String,
|
218
|
+
request_name: String,
|
219
|
+
params: T.untyped,
|
220
|
+
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
221
|
+
end
|
222
|
+
def delegate_request(server_addon_name:, request_name:, **params)
|
223
|
+
make_request(
|
224
|
+
"server_addon/delegate",
|
225
|
+
server_addon_name: server_addon_name,
|
226
|
+
request_name: request_name,
|
227
|
+
**params,
|
228
|
+
)
|
229
|
+
rescue MessageError
|
144
230
|
nil
|
145
231
|
end
|
146
232
|
|
147
233
|
sig { void }
|
148
234
|
def trigger_reload
|
149
|
-
|
235
|
+
log_message("Reloading Rails application")
|
150
236
|
send_notification("reload")
|
151
|
-
rescue
|
152
|
-
|
237
|
+
rescue MessageError
|
238
|
+
log_message(
|
239
|
+
"Ruby LSP Rails failed to trigger reload",
|
240
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
241
|
+
)
|
153
242
|
nil
|
154
243
|
end
|
155
244
|
|
156
245
|
sig { void }
|
157
246
|
def shutdown
|
158
|
-
|
247
|
+
log_message("Ruby LSP Rails shutting down server")
|
159
248
|
send_message("shutdown")
|
160
249
|
sleep(0.5) # give the server a bit of time to shutdown
|
161
250
|
[@stdin, @stdout, @stderr].each(&:close)
|
@@ -172,24 +261,24 @@ module RubyLsp
|
|
172
261
|
sig do
|
173
262
|
params(
|
174
263
|
request: String,
|
175
|
-
params: T.
|
264
|
+
params: T.untyped,
|
176
265
|
).returns(T.nilable(T::Hash[Symbol, T.untyped]))
|
177
266
|
end
|
178
|
-
def make_request(request, params
|
179
|
-
send_message(request, params)
|
267
|
+
def make_request(request, **params)
|
268
|
+
send_message(request, **params)
|
180
269
|
read_response
|
181
270
|
end
|
182
271
|
|
183
272
|
# Notifications are like messages, but one-way, with no response sent back.
|
184
|
-
sig { params(request: String, params: T.
|
185
|
-
def send_notification(request, params
|
273
|
+
sig { params(request: String, params: T.untyped).void }
|
274
|
+
def send_notification(request, **params) = send_message(request, **params)
|
186
275
|
|
187
276
|
private
|
188
277
|
|
189
|
-
sig { overridable.params(request: String, params: T.
|
190
|
-
def send_message(request, params
|
278
|
+
sig { overridable.params(request: String, params: T.untyped).void }
|
279
|
+
def send_message(request, **params)
|
191
280
|
message = { method: request }
|
192
|
-
message[:params] = params
|
281
|
+
message[:params] = params
|
193
282
|
json = message.to_json
|
194
283
|
|
195
284
|
@mutex.synchronize do
|
@@ -202,11 +291,9 @@ module RubyLsp
|
|
202
291
|
sig { overridable.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
203
292
|
def read_response
|
204
293
|
raw_response = @mutex.synchronize do
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
content_length = headers[/Content-Length: (\d+)/i, 1].to_i
|
209
|
-
raise EmptyMessageError if content_length.zero?
|
294
|
+
content_length = read_content_length
|
295
|
+
content_length = read_content_length unless content_length
|
296
|
+
raise EmptyMessageError unless content_length
|
210
297
|
|
211
298
|
@stdout.read(content_length)
|
212
299
|
end
|
@@ -214,7 +301,10 @@ module RubyLsp
|
|
214
301
|
response = JSON.parse(T.must(raw_response), symbolize_names: true)
|
215
302
|
|
216
303
|
if response[:error]
|
217
|
-
|
304
|
+
log_message(
|
305
|
+
"Ruby LSP Rails error: #{response[:error]}",
|
306
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
307
|
+
)
|
218
308
|
return
|
219
309
|
end
|
220
310
|
|
@@ -229,6 +319,24 @@ module RubyLsp
|
|
229
319
|
# Windows does not support the `TERM` signal, so we're forced to use `KILL` here
|
230
320
|
Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
|
231
321
|
end
|
322
|
+
|
323
|
+
sig { params(message: ::String, type: ::Integer).void }
|
324
|
+
def log_message(message, type: RubyLsp::Constant::MessageType::LOG)
|
325
|
+
return if @outgoing_queue.closed?
|
326
|
+
|
327
|
+
@outgoing_queue << RubyLsp::Notification.window_log_message(message, type: type)
|
328
|
+
end
|
329
|
+
|
330
|
+
sig { returns(T.nilable(Integer)) }
|
331
|
+
def read_content_length
|
332
|
+
headers = @stdout.gets("\r\n\r\n")
|
333
|
+
return unless headers
|
334
|
+
|
335
|
+
length = headers[/Content-Length: (\d+)/i, 1]
|
336
|
+
return unless length
|
337
|
+
|
338
|
+
length.to_i
|
339
|
+
end
|
232
340
|
end
|
233
341
|
|
234
342
|
class NullClient < RunnerClient
|
@@ -255,8 +363,13 @@ module RubyLsp
|
|
255
363
|
|
256
364
|
private
|
257
365
|
|
258
|
-
sig {
|
259
|
-
def
|
366
|
+
sig { params(message: ::String, type: ::Integer).void }
|
367
|
+
def log_message(message, type: RubyLsp::Constant::MessageType::LOG)
|
368
|
+
# no-op
|
369
|
+
end
|
370
|
+
|
371
|
+
sig { override.params(request: String, params: T.untyped).void }
|
372
|
+
def send_message(request, **params)
|
260
373
|
# no-op
|
261
374
|
end
|
262
375
|
|
@@ -2,13 +2,29 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "json"
|
5
|
+
require "open3"
|
5
6
|
|
6
7
|
# NOTE: We should avoid printing to stderr since it causes problems. We never read the standard error pipe from the
|
7
8
|
# client, so it will become full and eventually hang or crash. Instead, return a response with an `error` key.
|
8
9
|
|
9
10
|
module RubyLsp
|
10
11
|
module Rails
|
12
|
+
module Common
|
13
|
+
# Write a message to the client. Can be used for sending notifications to the editor
|
14
|
+
def send_message(message)
|
15
|
+
json_message = message.to_json
|
16
|
+
@stdout.write("Content-Length: #{json_message.length}\r\n\r\n#{json_message}")
|
17
|
+
end
|
18
|
+
|
19
|
+
# Log a message to the editor's output panel
|
20
|
+
def log_message(message)
|
21
|
+
$stderr.puts(message)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
11
25
|
class ServerAddon
|
26
|
+
include Common
|
27
|
+
|
12
28
|
@server_addon_classes = []
|
13
29
|
@server_addons = {}
|
14
30
|
|
@@ -38,12 +54,6 @@ module RubyLsp
|
|
38
54
|
@stdout = stdout
|
39
55
|
end
|
40
56
|
|
41
|
-
# Write a response back. Can be used for sending notifications to the editor
|
42
|
-
def write_response(response)
|
43
|
-
json_response = response.to_json
|
44
|
-
@stdout.write("Content-Length: #{json_response.length}\r\n\r\n#{json_response}")
|
45
|
-
end
|
46
|
-
|
47
57
|
def name
|
48
58
|
raise NotImplementedError, "Not implemented!"
|
49
59
|
end
|
@@ -54,6 +64,8 @@ module RubyLsp
|
|
54
64
|
end
|
55
65
|
|
56
66
|
class Server
|
67
|
+
include Common
|
68
|
+
|
57
69
|
def initialize(stdout: $stdout, override_default_output_device: true)
|
58
70
|
# Grab references to the original pipes so that we can change the default output device further down
|
59
71
|
@stdin = $stdin
|
@@ -79,8 +91,7 @@ module RubyLsp
|
|
79
91
|
routes_reloader = ::Rails.application.routes_reloader
|
80
92
|
routes_reloader.execute_unless_loaded if routes_reloader&.respond_to?(:execute_unless_loaded)
|
81
93
|
|
82
|
-
|
83
|
-
@stdout.write("Content-Length: #{initialize_result.length}\r\n\r\n#{initialize_result}")
|
94
|
+
send_message({ result: { message: "ok", root: ::Rails.root.to_s } })
|
84
95
|
|
85
96
|
while @running
|
86
97
|
headers = @stdin.gets("\r\n\r\n")
|
@@ -96,34 +107,38 @@ module RubyLsp
|
|
96
107
|
when "shutdown"
|
97
108
|
@running = false
|
98
109
|
when "model"
|
99
|
-
|
110
|
+
send_message(resolve_database_info_from_model(params.fetch(:name)))
|
100
111
|
when "association_target_location"
|
101
|
-
|
112
|
+
send_message(resolve_association_target(params))
|
113
|
+
when "pending_migrations_message"
|
114
|
+
send_message({ result: { pending_migrations_message: pending_migrations_message } })
|
115
|
+
when "run_migrations"
|
116
|
+
send_message({ result: run_migrations })
|
102
117
|
when "reload"
|
103
118
|
::Rails.application.reloader.reload!
|
104
119
|
when "route_location"
|
105
|
-
|
120
|
+
send_message(route_location(params.fetch(:name)))
|
106
121
|
when "route_info"
|
107
|
-
|
122
|
+
send_message(resolve_route_info(params))
|
108
123
|
when "server_addon/register"
|
109
124
|
require params[:server_addon_path]
|
110
125
|
ServerAddon.finalize_registrations!(@stdout)
|
111
126
|
when "server_addon/delegate"
|
112
|
-
server_addon_name = params
|
113
|
-
request_name = params
|
114
|
-
ServerAddon.delegate(server_addon_name, request_name, params)
|
127
|
+
server_addon_name = params[:server_addon_name]
|
128
|
+
request_name = params[:request_name]
|
129
|
+
ServerAddon.delegate(server_addon_name, request_name, params.except(:request_name, :server_addon_name))
|
115
130
|
end
|
131
|
+
request_name = request
|
132
|
+
request_name = "#{params[:server_addon_name]}##{params[:request_name]}" if request == "server_addon/delegate"
|
133
|
+
# Since this is a common problem, we show a specific error message to the user, instead of the full stack trace.
|
134
|
+
rescue ActiveRecord::ConnectionNotEstablished
|
135
|
+
log_message("Request #{request_name} failed because database connection was not established.")
|
116
136
|
rescue => e
|
117
|
-
|
137
|
+
log_message("Request #{request_name} failed:\n" + e.full_message(highlight: false))
|
118
138
|
end
|
119
139
|
|
120
140
|
private
|
121
141
|
|
122
|
-
def write_response(response)
|
123
|
-
json_response = response.to_json
|
124
|
-
@stdout.write("Content-Length: #{json_response.length}\r\n\r\n#{json_response}")
|
125
|
-
end
|
126
|
-
|
127
142
|
def resolve_route_info(requirements)
|
128
143
|
if requirements[:controller]
|
129
144
|
requirements[:controller] = requirements.fetch(:controller).underscore.delete_suffix("_controller")
|
@@ -136,8 +151,10 @@ module RubyLsp
|
|
136
151
|
::Rails.application.routes.routes.find { |route| route.requirements == requirements }
|
137
152
|
end
|
138
153
|
|
139
|
-
|
140
|
-
|
154
|
+
source_location = route&.respond_to?(:source_location) && route.source_location
|
155
|
+
|
156
|
+
if source_location
|
157
|
+
file, _, line = source_location.rpartition(":")
|
141
158
|
body = {
|
142
159
|
source_location: [::Rails.root.join(file).to_s, line],
|
143
160
|
verb: route.verb,
|
@@ -155,6 +172,11 @@ module RubyLsp
|
|
155
172
|
if ActionDispatch::Routing::Mapper.respond_to?(:route_source_locations) &&
|
156
173
|
ActionDispatch::Routing::Mapper.route_source_locations
|
157
174
|
def route_location(name)
|
175
|
+
# In Rails 8, Rails.application.routes.named_routes is not populated by default
|
176
|
+
if ::Rails.application.respond_to?(:reload_routes_unless_loaded)
|
177
|
+
::Rails.application.reload_routes_unless_loaded
|
178
|
+
end
|
179
|
+
|
158
180
|
match_data = name.match(/^(.+)(_path|_url)$/)
|
159
181
|
return { result: nil } unless match_data
|
160
182
|
|
@@ -188,7 +210,7 @@ module RubyLsp
|
|
188
210
|
|
189
211
|
info = {
|
190
212
|
result: {
|
191
|
-
columns: const.columns.map { |column| [column.name, column.type] },
|
213
|
+
columns: const.columns.map { |column| [column.name, column.type, column.default, column.null] },
|
192
214
|
primary_keys: Array(const.primary_key),
|
193
215
|
},
|
194
216
|
}
|
@@ -235,6 +257,27 @@ module RubyLsp
|
|
235
257
|
!const.abstract_class?
|
236
258
|
)
|
237
259
|
end
|
260
|
+
|
261
|
+
def pending_migrations_message
|
262
|
+
# `check_all_pending!` is only available since Rails 7.1
|
263
|
+
return unless defined?(ActiveRecord) && ActiveRecord::Migration.respond_to?(:check_all_pending!)
|
264
|
+
|
265
|
+
ActiveRecord::Migration.check_all_pending!
|
266
|
+
nil
|
267
|
+
rescue ActiveRecord::PendingMigrationError => e
|
268
|
+
e.message
|
269
|
+
end
|
270
|
+
|
271
|
+
def run_migrations
|
272
|
+
# Running migrations invokes `load` which will repeatedly load the same files. It's not designed to be invoked
|
273
|
+
# multiple times within the same process. To avoid any memory bloat, we run migrations in a separate process
|
274
|
+
stdout, status = Open3.capture2(
|
275
|
+
{ "VERBOSE" => "true" },
|
276
|
+
"bundle exec rails db:migrate",
|
277
|
+
)
|
278
|
+
|
279
|
+
{ message: stdout, status: status.exitstatus }
|
280
|
+
end
|
238
281
|
end
|
239
282
|
end
|
240
283
|
end
|
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.25
|
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-11-06 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.21.2
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: 0.
|
22
|
+
version: 0.22.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.21.2
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: 0.
|
32
|
+
version: 0.22.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
|
@@ -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.23
|
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
|