ruby-lsp-rails 0.3.8 → 0.3.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -0
- data/lib/ruby_lsp/ruby_lsp_rails/addon.rb +7 -4
- data/lib/ruby_lsp/ruby_lsp_rails/code_lens.rb +155 -9
- data/lib/ruby_lsp/ruby_lsp_rails/definition.rb +4 -2
- data/lib/ruby_lsp/ruby_lsp_rails/indexing_enhancement.rb +113 -0
- data/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb +50 -13
- data/lib/ruby_lsp/ruby_lsp_rails/server.rb +29 -1
- data/lib/ruby_lsp_rails/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e2543aa7d39f0a6b231112f9ddb365692c7e8d06eb14681da551ab55fd8f5a4d
|
4
|
+
data.tar.gz: e466ccd6d90934e083e638efdcedfb5d6b8f85fbce70b139e1542eeb2ab8a139
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f7c82462682d93fd6b944c2705c8b62b424b4abc3636d1f2540eeee1ab9f7e2896eb77d338697bb65a0a64752529085f38e49bd5a75bfc668fa9d31dd5b0d10
|
7
|
+
data.tar.gz: a8978fdebb07ba8b76dd10a725b3b3c736961f103cefa806fbc0302b494516e0a196105bf7fac9d114c963a62c8a706959d53f1509a4ed343e2e9d24633c4372
|
data/README.md
CHANGED
@@ -9,6 +9,8 @@ Ruby LSP Rails is a [Ruby LSP](https://github.com/Shopify/ruby-lsp) addon for ex
|
|
9
9
|
* Navigate to associations, validations, callbacks and test cases using your editor's "Go to Symbol" feature, or outline view.
|
10
10
|
* Jump to the definition of callbacks using your editor's "Go to Definition" feature.
|
11
11
|
* Jump to the declaration of a route.
|
12
|
+
* Code Lens allowing fast-forwarding or rewinding of migrations.
|
13
|
+
* Code Lens showing the path that a route action corresponds to.
|
12
14
|
|
13
15
|
## Installation
|
14
16
|
|
@@ -13,6 +13,7 @@ require_relative "hover"
|
|
13
13
|
require_relative "code_lens"
|
14
14
|
require_relative "document_symbol"
|
15
15
|
require_relative "definition"
|
16
|
+
require_relative "indexing_enhancement"
|
16
17
|
|
17
18
|
module RubyLsp
|
18
19
|
module Rails
|
@@ -35,6 +36,8 @@ module RubyLsp
|
|
35
36
|
# Start booting the real client in a background thread. Until this completes, the client will be a NullClient
|
36
37
|
Thread.new { @client = RunnerClient.create_client }
|
37
38
|
register_additional_file_watchers(global_state: global_state, message_queue: message_queue)
|
39
|
+
|
40
|
+
T.must(@global_state).index.register_enhancement(IndexingEnhancement.new)
|
38
41
|
end
|
39
42
|
|
40
43
|
sig { override.void }
|
@@ -51,9 +54,7 @@ module RubyLsp
|
|
51
54
|
).void
|
52
55
|
end
|
53
56
|
def create_code_lens_listener(response_builder, uri, dispatcher)
|
54
|
-
|
55
|
-
|
56
|
-
CodeLens.new(response_builder, uri, dispatcher)
|
57
|
+
CodeLens.new(@client, T.must(@global_state), response_builder, uri, dispatcher)
|
57
58
|
end
|
58
59
|
|
59
60
|
sig do
|
@@ -79,7 +80,9 @@ module RubyLsp
|
|
79
80
|
|
80
81
|
sig do
|
81
82
|
override.params(
|
82
|
-
response_builder: ResponseBuilders::CollectionResponseBuilder[
|
83
|
+
response_builder: ResponseBuilders::CollectionResponseBuilder[T.any(
|
84
|
+
Interface::Location, Interface::LocationLink
|
85
|
+
)],
|
83
86
|
uri: URI::Generic,
|
84
87
|
node_context: NodeContext,
|
85
88
|
dispatcher: Prism::Dispatcher,
|
@@ -5,11 +5,22 @@ module RubyLsp
|
|
5
5
|
module Rails
|
6
6
|
# ![CodeLens demo](../../code_lens.gif)
|
7
7
|
#
|
8
|
-
# This feature adds
|
8
|
+
# This feature adds Code Lens features for Rails applications.
|
9
|
+
#
|
10
|
+
# For Active Support test cases:
|
9
11
|
#
|
10
12
|
# - Run tests in the VS Terminal
|
11
13
|
# - Run tests in the VS Code Test Explorer
|
12
14
|
# - Debug tests
|
15
|
+
# - Run migrations in the VS Terminal
|
16
|
+
#
|
17
|
+
# For Rails controllers:
|
18
|
+
#
|
19
|
+
# - See the path corresponding to an action
|
20
|
+
# - Click on the action's Code Lens to jump to its declaration in the routes.
|
21
|
+
#
|
22
|
+
# Note: This depends on a support for the `rubyLsp.openFile` command.
|
23
|
+
# For the VS Code extension this is built-in, but for other editors this may require some custom configuration.
|
13
24
|
#
|
14
25
|
# The
|
15
26
|
# [code lens](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens)
|
@@ -32,12 +43,34 @@ module RubyLsp
|
|
32
43
|
# # ...
|
33
44
|
# end
|
34
45
|
# end
|
35
|
-
#
|
46
|
+
# ```
|
47
|
+
#
|
48
|
+
# # Example:
|
49
|
+
# ```ruby
|
50
|
+
# Run
|
51
|
+
# class AddFirstNameToUsers < ActiveRecord::Migration[7.1]
|
52
|
+
# # ...
|
53
|
+
# end
|
54
|
+
# ```
|
36
55
|
#
|
37
56
|
# The code lenses will be displayed above the class and above each test method.
|
38
57
|
#
|
39
58
|
# Note: When using the Test Explorer view, if your code contains a statement to pause execution (e.g. `debugger`) it
|
40
59
|
# will cause the test runner to hang.
|
60
|
+
#
|
61
|
+
# For the following code, assuming the routing contains `resources :users`, a Code Lens will be seen above each
|
62
|
+
# action.
|
63
|
+
#
|
64
|
+
# ```ruby
|
65
|
+
# class UsersController < ApplicationController
|
66
|
+
# GET /users(.:format)
|
67
|
+
# def index # <- Will show code lens above for the path
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
# ```
|
71
|
+
#
|
72
|
+
# Note: Complex routing configurations may not be supported.
|
73
|
+
#
|
41
74
|
class CodeLens
|
42
75
|
extend T::Sig
|
43
76
|
include Requests::Support::Common
|
@@ -45,18 +78,31 @@ module RubyLsp
|
|
45
78
|
|
46
79
|
sig do
|
47
80
|
params(
|
81
|
+
client: RunnerClient,
|
82
|
+
global_state: GlobalState,
|
48
83
|
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
|
49
84
|
uri: URI::Generic,
|
50
85
|
dispatcher: Prism::Dispatcher,
|
51
86
|
).void
|
52
87
|
end
|
53
|
-
def initialize(response_builder, uri, dispatcher)
|
88
|
+
def initialize(client, global_state, response_builder, uri, dispatcher)
|
89
|
+
@client = client
|
90
|
+
@global_state = global_state
|
54
91
|
@response_builder = response_builder
|
55
92
|
@path = T.let(uri.to_standardized_path, T.nilable(String))
|
56
93
|
@group_id = T.let(1, Integer)
|
57
94
|
@group_id_stack = T.let([], T::Array[Integer])
|
95
|
+
@constant_name_stack = T.let([], T::Array[[String, T.nilable(String)]])
|
58
96
|
|
59
|
-
dispatcher.register(
|
97
|
+
dispatcher.register(
|
98
|
+
self,
|
99
|
+
:on_call_node_enter,
|
100
|
+
:on_class_node_enter,
|
101
|
+
:on_def_node_enter,
|
102
|
+
:on_class_node_leave,
|
103
|
+
:on_module_node_enter,
|
104
|
+
:on_module_node_leave,
|
105
|
+
)
|
60
106
|
end
|
61
107
|
|
62
108
|
sig { params(node: Prism::CallNode).void }
|
@@ -74,46 +120,146 @@ module RubyLsp
|
|
74
120
|
sig { params(node: Prism::DefNode).void }
|
75
121
|
def on_def_node_enter(node)
|
76
122
|
method_name = node.name.to_s
|
123
|
+
|
77
124
|
if method_name.start_with?("test_")
|
78
125
|
line_number = node.location.start_line
|
79
126
|
command = "#{test_command} #{@path}:#{line_number}"
|
80
127
|
add_test_code_lens(node, name: method_name, command: command, kind: :example)
|
81
128
|
end
|
129
|
+
|
130
|
+
if controller?
|
131
|
+
add_route_code_lens_to_action(node)
|
132
|
+
add_jump_to_view(node)
|
133
|
+
end
|
82
134
|
end
|
83
135
|
|
84
136
|
sig { params(node: Prism::ClassNode).void }
|
85
137
|
def on_class_node_enter(node)
|
86
138
|
class_name = node.constant_path.slice
|
139
|
+
superclass_name = node.superclass&.slice
|
140
|
+
|
87
141
|
if class_name.end_with?("Test")
|
88
142
|
command = "#{test_command} #{@path}"
|
89
143
|
add_test_code_lens(node, name: class_name, command: command, kind: :group)
|
90
144
|
@group_id_stack.push(@group_id)
|
91
145
|
@group_id += 1
|
92
146
|
end
|
147
|
+
|
148
|
+
if superclass_name&.start_with?("ActiveRecord::Migration")
|
149
|
+
command = "#{migrate_command} VERSION=#{migration_version}"
|
150
|
+
add_migrate_code_lens(node, name: class_name, command: command)
|
151
|
+
end
|
152
|
+
|
153
|
+
# We need to use a stack because someone could define a nested class
|
154
|
+
# inside a controller. When we exit that nested class declaration, we are
|
155
|
+
# back in a controller context. This part is used in other places in the LSP
|
156
|
+
@constant_name_stack << [class_name, superclass_name]
|
93
157
|
end
|
94
158
|
|
95
159
|
sig { params(node: Prism::ClassNode).void }
|
96
160
|
def on_class_node_leave(node)
|
97
161
|
class_name = node.constant_path.slice
|
162
|
+
|
98
163
|
if class_name.end_with?("Test")
|
99
164
|
@group_id_stack.pop
|
100
165
|
end
|
166
|
+
|
167
|
+
@constant_name_stack.pop
|
168
|
+
end
|
169
|
+
|
170
|
+
sig { params(node: Prism::ModuleNode).void }
|
171
|
+
def on_module_node_enter(node)
|
172
|
+
@constant_name_stack << [node.constant_path.slice, nil]
|
173
|
+
end
|
174
|
+
|
175
|
+
sig { params(node: Prism::ModuleNode).void }
|
176
|
+
def on_module_node_leave(node)
|
177
|
+
@constant_name_stack.pop
|
101
178
|
end
|
102
179
|
|
103
180
|
private
|
104
181
|
|
182
|
+
sig { returns(T.nilable(T::Boolean)) }
|
183
|
+
def controller?
|
184
|
+
class_name, superclass_name = @constant_name_stack.last
|
185
|
+
return false unless class_name && superclass_name
|
186
|
+
|
187
|
+
class_name.end_with?("Controller") && superclass_name.end_with?("Controller")
|
188
|
+
end
|
189
|
+
|
190
|
+
sig { params(node: Prism::DefNode).void }
|
191
|
+
def add_jump_to_view(node)
|
192
|
+
class_name = @constant_name_stack.map(&:first).join("::")
|
193
|
+
action_name = node.name
|
194
|
+
controller_name = class_name
|
195
|
+
.delete_suffix("Controller")
|
196
|
+
.gsub(/([a-z])([A-Z])/, "\\1_\\2")
|
197
|
+
.gsub("::", "/")
|
198
|
+
.downcase
|
199
|
+
|
200
|
+
view_uris = Dir.glob("#{@client.rails_root}/app/views/#{controller_name}/#{action_name}*").map! do |path|
|
201
|
+
URI::Generic.from_path(path: path).to_s
|
202
|
+
end
|
203
|
+
return if view_uris.empty?
|
204
|
+
|
205
|
+
@response_builder << create_code_lens(
|
206
|
+
node,
|
207
|
+
title: "Jump to view",
|
208
|
+
command_name: "rubyLsp.openFile",
|
209
|
+
arguments: [view_uris],
|
210
|
+
data: { type: "file" },
|
211
|
+
)
|
212
|
+
end
|
213
|
+
|
214
|
+
sig { params(node: Prism::DefNode).void }
|
215
|
+
def add_route_code_lens_to_action(node)
|
216
|
+
class_name, _ = T.must(@constant_name_stack.last)
|
217
|
+
route = @client.route(controller: class_name, action: node.name.to_s)
|
218
|
+
return unless route
|
219
|
+
|
220
|
+
file_path, line = route[:source_location]
|
221
|
+
|
222
|
+
@response_builder << create_code_lens(
|
223
|
+
node,
|
224
|
+
title: "#{route[:verb]} #{route[:path]}",
|
225
|
+
command_name: "rubyLsp.openFile",
|
226
|
+
arguments: [["file://#{file_path}#L#{line}"]],
|
227
|
+
data: { type: "file" },
|
228
|
+
)
|
229
|
+
end
|
230
|
+
|
105
231
|
sig { returns(String) }
|
106
232
|
def test_command
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
233
|
+
"#{RbConfig.ruby} bin/rails test"
|
234
|
+
end
|
235
|
+
|
236
|
+
sig { returns(String) }
|
237
|
+
def migrate_command
|
238
|
+
"#{RbConfig.ruby} bin/rails db:migrate"
|
239
|
+
end
|
240
|
+
|
241
|
+
sig { returns(T.nilable(String)) }
|
242
|
+
def migration_version
|
243
|
+
File.basename(T.must(@path)).split("_").first
|
244
|
+
end
|
245
|
+
|
246
|
+
sig { params(node: Prism::Node, name: String, command: String).void }
|
247
|
+
def add_migrate_code_lens(node, name:, command:)
|
248
|
+
return unless @path
|
249
|
+
|
250
|
+
@response_builder << create_code_lens(
|
251
|
+
node,
|
252
|
+
title: "Run",
|
253
|
+
command_name: "rubyLsp.runTask",
|
254
|
+
arguments: [command],
|
255
|
+
data: { type: "migrate" },
|
256
|
+
)
|
112
257
|
end
|
113
258
|
|
114
259
|
sig { params(node: Prism::Node, name: String, command: String, kind: Symbol).void }
|
115
260
|
def add_test_code_lens(node, name:, command:, kind:)
|
116
261
|
return unless @path
|
262
|
+
return unless @global_state.test_library == "rails"
|
117
263
|
|
118
264
|
arguments = [
|
119
265
|
@path,
|
@@ -17,7 +17,7 @@ module RubyLsp
|
|
17
17
|
# # Example
|
18
18
|
#
|
19
19
|
# ```ruby
|
20
|
-
# before_action :foo # <- Go to definition on this symbol will jump to the method
|
20
|
+
# before_action :foo # <- Go to definition on this symbol will jump to the method
|
21
21
|
# ```
|
22
22
|
#
|
23
23
|
# Notes for named routes:
|
@@ -34,7 +34,9 @@ module RubyLsp
|
|
34
34
|
sig do
|
35
35
|
params(
|
36
36
|
client: RunnerClient,
|
37
|
-
response_builder: ResponseBuilders::CollectionResponseBuilder[
|
37
|
+
response_builder: RubyLsp::ResponseBuilders::CollectionResponseBuilder[T.any(
|
38
|
+
Interface::Location, Interface::LocationLink
|
39
|
+
)],
|
38
40
|
node_context: NodeContext,
|
39
41
|
index: RubyIndexer::Index,
|
40
42
|
dispatcher: Prism::Dispatcher,
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
module Rails
|
6
|
+
class IndexingEnhancement
|
7
|
+
extend T::Sig
|
8
|
+
include RubyIndexer::Enhancement
|
9
|
+
|
10
|
+
sig do
|
11
|
+
override.params(
|
12
|
+
index: RubyIndexer::Index,
|
13
|
+
owner: T.nilable(RubyIndexer::Entry::Namespace),
|
14
|
+
node: Prism::CallNode,
|
15
|
+
file_path: String,
|
16
|
+
).void
|
17
|
+
end
|
18
|
+
def on_call_node(index, owner, node, file_path)
|
19
|
+
return unless owner
|
20
|
+
|
21
|
+
name = node.name
|
22
|
+
|
23
|
+
case name
|
24
|
+
when :extend
|
25
|
+
handle_concern_extend(index, owner, node)
|
26
|
+
when :has_one, :has_many, :belongs_to, :has_and_belongs_to_many
|
27
|
+
handle_association(index, owner, node, file_path)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
sig do
|
34
|
+
params(
|
35
|
+
index: RubyIndexer::Index,
|
36
|
+
owner: RubyIndexer::Entry::Namespace,
|
37
|
+
node: Prism::CallNode,
|
38
|
+
file_path: String,
|
39
|
+
).void
|
40
|
+
end
|
41
|
+
def handle_association(index, owner, node, file_path)
|
42
|
+
arguments = node.arguments&.arguments
|
43
|
+
return unless arguments
|
44
|
+
|
45
|
+
name_arg = arguments.first
|
46
|
+
|
47
|
+
name = case name_arg
|
48
|
+
when Prism::StringNode
|
49
|
+
name_arg.content
|
50
|
+
when Prism::SymbolNode
|
51
|
+
name_arg.value
|
52
|
+
end
|
53
|
+
|
54
|
+
return unless name
|
55
|
+
|
56
|
+
# Reader
|
57
|
+
index.add(RubyIndexer::Entry::Method.new(
|
58
|
+
name,
|
59
|
+
file_path,
|
60
|
+
name_arg.location,
|
61
|
+
name_arg.location,
|
62
|
+
[],
|
63
|
+
[RubyIndexer::Entry::Signature.new([])],
|
64
|
+
RubyIndexer::Entry::Visibility::PUBLIC,
|
65
|
+
owner,
|
66
|
+
))
|
67
|
+
|
68
|
+
# Writer
|
69
|
+
index.add(RubyIndexer::Entry::Method.new(
|
70
|
+
"#{name}=",
|
71
|
+
file_path,
|
72
|
+
name_arg.location,
|
73
|
+
name_arg.location,
|
74
|
+
[],
|
75
|
+
[RubyIndexer::Entry::Signature.new([RubyIndexer::Entry::RequiredParameter.new(name: name.to_sym)])],
|
76
|
+
RubyIndexer::Entry::Visibility::PUBLIC,
|
77
|
+
owner,
|
78
|
+
))
|
79
|
+
end
|
80
|
+
|
81
|
+
sig do
|
82
|
+
params(
|
83
|
+
index: RubyIndexer::Index,
|
84
|
+
owner: RubyIndexer::Entry::Namespace,
|
85
|
+
node: Prism::CallNode,
|
86
|
+
).void
|
87
|
+
end
|
88
|
+
def handle_concern_extend(index, owner, node)
|
89
|
+
arguments = node.arguments&.arguments
|
90
|
+
return unless arguments
|
91
|
+
|
92
|
+
arguments.each do |node|
|
93
|
+
next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
94
|
+
|
95
|
+
module_name = node.full_name
|
96
|
+
next unless module_name == "ActiveSupport::Concern"
|
97
|
+
|
98
|
+
index.register_included_hook(owner.name) do |index, base|
|
99
|
+
class_methods_name = "#{owner.name}::ClassMethods"
|
100
|
+
|
101
|
+
if index.indexed?(class_methods_name)
|
102
|
+
singleton = index.existing_or_new_singleton_class(base.name)
|
103
|
+
singleton.mixin_operations << RubyIndexer::Entry::Include.new(class_methods_name)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
|
107
|
+
Prism::ConstantPathNode::MissingNodesInConstantPathError
|
108
|
+
# Do nothing
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -36,8 +36,12 @@ module RubyLsp
|
|
36
36
|
|
37
37
|
extend T::Sig
|
38
38
|
|
39
|
+
sig { returns(String) }
|
40
|
+
attr_reader :rails_root
|
41
|
+
|
39
42
|
sig { void }
|
40
43
|
def initialize
|
44
|
+
@mutex = T.let(Mutex.new, Mutex)
|
41
45
|
# Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
|
42
46
|
# parent ends, the spring process ends as well. If this is not set, Spring will throw an error while trying to
|
43
47
|
# set its own session ID
|
@@ -67,7 +71,8 @@ module RubyLsp
|
|
67
71
|
|
68
72
|
begin
|
69
73
|
count += 1
|
70
|
-
read_response
|
74
|
+
initialize_response = T.must(read_response)
|
75
|
+
@rails_root = T.let(initialize_response[:root], String)
|
71
76
|
rescue EmptyMessageError
|
72
77
|
$stderr.puts("Ruby LSP Rails is retrying initialize (#{count})")
|
73
78
|
retry if count < MAX_RETRIES
|
@@ -80,9 +85,7 @@ module RubyLsp
|
|
80
85
|
if @wait_thread.alive?
|
81
86
|
$stderr.puts("Ruby LSP Rails is force killing the server")
|
82
87
|
sleep(0.5) # give the server a bit of time if we already issued a shutdown notification
|
83
|
-
|
84
|
-
# Windows does not support the `TERM` signal, so we're forced to use `KILL` here
|
85
|
-
Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
|
88
|
+
force_kill
|
86
89
|
end
|
87
90
|
end
|
88
91
|
end
|
@@ -122,6 +125,14 @@ module RubyLsp
|
|
122
125
|
nil
|
123
126
|
end
|
124
127
|
|
128
|
+
sig { params(controller: String, action: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
129
|
+
def route(controller:, action:)
|
130
|
+
make_request("route_info", controller: controller, action: action)
|
131
|
+
rescue IncompleteMessageError
|
132
|
+
$stderr.puts("Ruby LSP Rails failed to get route information: #{@stderr.read}")
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
|
125
136
|
sig { void }
|
126
137
|
def trigger_reload
|
127
138
|
$stderr.puts("Reloading Rails application")
|
@@ -137,6 +148,9 @@ module RubyLsp
|
|
137
148
|
send_message("shutdown")
|
138
149
|
sleep(0.5) # give the server a bit of time to shutdown
|
139
150
|
[@stdin, @stdout, @stderr].each(&:close)
|
151
|
+
rescue IOError
|
152
|
+
# The server connection may have died
|
153
|
+
force_kill
|
140
154
|
end
|
141
155
|
|
142
156
|
sig { returns(T::Boolean) }
|
@@ -157,26 +171,35 @@ module RubyLsp
|
|
157
171
|
read_response
|
158
172
|
end
|
159
173
|
|
160
|
-
sig { params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
|
174
|
+
sig { overridable.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
|
161
175
|
def send_message(request, params = nil)
|
162
176
|
message = { method: request }
|
163
177
|
message[:params] = params if params
|
164
178
|
json = message.to_json
|
165
179
|
|
166
|
-
@
|
180
|
+
@mutex.synchronize do
|
181
|
+
@stdin.write("Content-Length: #{json.length}\r\n\r\n", json)
|
182
|
+
end
|
183
|
+
rescue Errno::EPIPE
|
184
|
+
# The server connection died
|
167
185
|
end
|
168
186
|
|
169
|
-
|
187
|
+
# Notifications are like messages, but one-way, with no response sent back.
|
188
|
+
sig { params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
|
189
|
+
def send_notification(request, params = nil) = send_message(request, params)
|
170
190
|
|
171
|
-
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
191
|
+
sig { overridable.returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
172
192
|
def read_response
|
173
|
-
|
174
|
-
|
193
|
+
raw_response = @mutex.synchronize do
|
194
|
+
headers = @stdout.gets("\r\n\r\n")
|
195
|
+
raise IncompleteMessageError unless headers
|
175
196
|
|
176
|
-
|
177
|
-
|
197
|
+
content_length = headers[/Content-Length: (\d+)/i, 1].to_i
|
198
|
+
raise EmptyMessageError if content_length.zero?
|
199
|
+
|
200
|
+
@stdout.read(content_length)
|
201
|
+
end
|
178
202
|
|
179
|
-
raw_response = @stdout.read(content_length)
|
180
203
|
response = JSON.parse(T.must(raw_response), symbolize_names: true)
|
181
204
|
|
182
205
|
if response[:error]
|
@@ -185,6 +208,15 @@ module RubyLsp
|
|
185
208
|
end
|
186
209
|
|
187
210
|
response.fetch(:result)
|
211
|
+
rescue Errno::EPIPE
|
212
|
+
# The server connection died
|
213
|
+
nil
|
214
|
+
end
|
215
|
+
|
216
|
+
sig { void }
|
217
|
+
def force_kill
|
218
|
+
# Windows does not support the `TERM` signal, so we're forced to use `KILL` here
|
219
|
+
Process.kill(T.must(Signal.list["KILL"]), @wait_thread.pid)
|
188
220
|
end
|
189
221
|
end
|
190
222
|
|
@@ -205,6 +237,11 @@ module RubyLsp
|
|
205
237
|
true
|
206
238
|
end
|
207
239
|
|
240
|
+
sig { override.returns(String) }
|
241
|
+
def rails_root
|
242
|
+
Dir.pwd
|
243
|
+
end
|
244
|
+
|
208
245
|
private
|
209
246
|
|
210
247
|
sig { override.params(request: String, params: T.nilable(T::Hash[Symbol, T.untyped])).void }
|
@@ -24,7 +24,7 @@ module RubyLsp
|
|
24
24
|
routes_reloader = ::Rails.application.routes_reloader
|
25
25
|
routes_reloader.execute_unless_loaded if routes_reloader&.respond_to?(:execute_unless_loaded)
|
26
26
|
|
27
|
-
initialize_result = { result: { message: "ok" } }.to_json
|
27
|
+
initialize_result = { result: { message: "ok", root: ::Rails.root.to_s } }.to_json
|
28
28
|
$stdout.write("Content-Length: #{initialize_result.length}\r\n\r\n#{initialize_result}")
|
29
29
|
|
30
30
|
while @running
|
@@ -54,6 +54,8 @@ module RubyLsp
|
|
54
54
|
VOID
|
55
55
|
when "route_location"
|
56
56
|
route_location(params.fetch(:name))
|
57
|
+
when "route_info"
|
58
|
+
resolve_route_info(params)
|
57
59
|
else
|
58
60
|
VOID
|
59
61
|
end
|
@@ -63,6 +65,32 @@ module RubyLsp
|
|
63
65
|
|
64
66
|
private
|
65
67
|
|
68
|
+
def resolve_route_info(requirements)
|
69
|
+
if requirements[:controller]
|
70
|
+
requirements[:controller] = requirements.fetch(:controller).underscore.delete_suffix("_controller")
|
71
|
+
end
|
72
|
+
|
73
|
+
# In Rails 7.2 we can use `from_requirements, otherwise we fall back to a private API
|
74
|
+
route = if ::Rails.application.routes.respond_to?(:from_requirements)
|
75
|
+
::Rails.application.routes.from_requirements(requirements)
|
76
|
+
else
|
77
|
+
::Rails.application.routes.routes.find { |route| route.requirements == requirements }
|
78
|
+
end
|
79
|
+
|
80
|
+
if route&.source_location
|
81
|
+
file, _, line = route.source_location.rpartition(":")
|
82
|
+
body = {
|
83
|
+
source_location: [::Rails.root.join(file).to_s, line],
|
84
|
+
verb: route.verb,
|
85
|
+
path: route.path.spec.to_s,
|
86
|
+
}
|
87
|
+
|
88
|
+
{ result: body }
|
89
|
+
else
|
90
|
+
{ result: nil }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
66
94
|
# Older versions of Rails don't support `route_source_locations`.
|
67
95
|
# We also check that it's enabled.
|
68
96
|
if ActionDispatch::Routing::Mapper.respond_to?(:route_source_locations) &&
|
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.13
|
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-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby-lsp
|
@@ -16,7 +16,7 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.17.
|
19
|
+
version: 0.17.12
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
22
|
version: 0.18.0
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 0.17.
|
29
|
+
version: 0.17.12
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 0.18.0
|
@@ -46,6 +46,7 @@ files:
|
|
46
46
|
- lib/ruby_lsp/ruby_lsp_rails/definition.rb
|
47
47
|
- lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb
|
48
48
|
- lib/ruby_lsp/ruby_lsp_rails/hover.rb
|
49
|
+
- lib/ruby_lsp/ruby_lsp_rails/indexing_enhancement.rb
|
49
50
|
- lib/ruby_lsp/ruby_lsp_rails/runner_client.rb
|
50
51
|
- lib/ruby_lsp/ruby_lsp_rails/server.rb
|
51
52
|
- lib/ruby_lsp/ruby_lsp_rails/support/active_support_test_case_helper.rb
|
@@ -79,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
80
|
- !ruby/object:Gem::Version
|
80
81
|
version: '0'
|
81
82
|
requirements: []
|
82
|
-
rubygems_version: 3.5.
|
83
|
+
rubygems_version: 3.5.17
|
83
84
|
signing_key:
|
84
85
|
specification_version: 4
|
85
86
|
summary: A Ruby LSP addon for Rails
|