ruby-lsp-rails 0.3.17 → 0.3.25
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/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
|
# 
|
@@ -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
|