ruby-lsp-rails 0.4.0 → 0.4.2
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/Rakefile +1 -1
- data/lib/ruby_lsp/ruby_lsp_rails/addon.rb +59 -68
- data/lib/ruby_lsp/ruby_lsp_rails/code_lens.rb +23 -30
- data/lib/ruby_lsp/ruby_lsp_rails/completion.rb +7 -15
- data/lib/ruby_lsp/ruby_lsp_rails/definition.rb +13 -22
- data/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +24 -30
- data/lib/ruby_lsp/ruby_lsp_rails/hover.rb +41 -29
- data/lib/ruby_lsp/ruby_lsp_rails/indexing_enhancement.rb +7 -20
- data/lib/ruby_lsp/ruby_lsp_rails/rails_test_style.rb +150 -0
- data/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb +64 -79
- data/lib/ruby_lsp/ruby_lsp_rails/server.rb +117 -31
- data/lib/ruby_lsp/ruby_lsp_rails/support/active_support_test_case_helper.rb +3 -4
- data/lib/ruby_lsp/ruby_lsp_rails/support/associations.rb +6 -9
- data/lib/ruby_lsp/ruby_lsp_rails/support/callbacks.rb +48 -57
- data/lib/ruby_lsp/ruby_lsp_rails/support/location_builder.rb +1 -3
- data/lib/ruby_lsp_rails/version.rb +1 -1
- metadata +6 -5
@@ -9,19 +9,13 @@ module RubyLsp
|
|
9
9
|
# request allows users to navigate between associations, validations, callbacks and ActiveSupport test cases with
|
10
10
|
# VS Code's "Go to Symbol" feature.
|
11
11
|
class DocumentSymbol
|
12
|
-
extend T::Sig
|
13
12
|
include Requests::Support::Common
|
14
13
|
include ActiveSupportTestCaseHelper
|
15
14
|
|
16
|
-
|
17
|
-
params(
|
18
|
-
response_builder: ResponseBuilders::DocumentSymbol,
|
19
|
-
dispatcher: Prism::Dispatcher,
|
20
|
-
).void
|
21
|
-
end
|
15
|
+
#: (ResponseBuilders::DocumentSymbol response_builder, Prism::Dispatcher dispatcher) -> void
|
22
16
|
def initialize(response_builder, dispatcher)
|
23
17
|
@response_builder = response_builder
|
24
|
-
@namespace_stack =
|
18
|
+
@namespace_stack = [] #: Array[String]
|
25
19
|
|
26
20
|
dispatcher.register(
|
27
21
|
self,
|
@@ -33,7 +27,7 @@ module RubyLsp
|
|
33
27
|
)
|
34
28
|
end
|
35
29
|
|
36
|
-
|
30
|
+
#: (Prism::CallNode node) -> void
|
37
31
|
def on_call_node_enter(node)
|
38
32
|
return if @namespace_stack.empty?
|
39
33
|
|
@@ -51,50 +45,52 @@ module RubyLsp
|
|
51
45
|
return if receiver && !receiver.is_a?(Prism::SelfNode)
|
52
46
|
|
53
47
|
message = node.message
|
48
|
+
return unless message
|
49
|
+
|
54
50
|
case message
|
55
51
|
when *Support::Callbacks::ALL, "validate"
|
56
|
-
handle_all_arg_types(node,
|
52
|
+
handle_all_arg_types(node, message)
|
57
53
|
when "validates", "validates!", "validates_each", "belongs_to", "has_one", "has_many",
|
58
54
|
"has_and_belongs_to_many", "attr_readonly", "scope"
|
59
|
-
handle_symbol_and_string_arg_types(node,
|
55
|
+
handle_symbol_and_string_arg_types(node, message)
|
60
56
|
when "validates_with"
|
61
|
-
handle_class_arg_types(node,
|
57
|
+
handle_class_arg_types(node, message)
|
62
58
|
end
|
63
59
|
end
|
64
60
|
|
65
|
-
|
61
|
+
#: (Prism::ClassNode node) -> void
|
66
62
|
def on_class_node_enter(node)
|
67
63
|
add_to_namespace_stack(node)
|
68
64
|
end
|
69
65
|
|
70
|
-
|
66
|
+
#: (Prism::ClassNode node) -> void
|
71
67
|
def on_class_node_leave(node)
|
72
68
|
remove_from_namespace_stack(node)
|
73
69
|
end
|
74
70
|
|
75
|
-
|
71
|
+
#: (Prism::ModuleNode node) -> void
|
76
72
|
def on_module_node_enter(node)
|
77
73
|
add_to_namespace_stack(node)
|
78
74
|
end
|
79
75
|
|
80
|
-
|
76
|
+
#: (Prism::ModuleNode node) -> void
|
81
77
|
def on_module_node_leave(node)
|
82
78
|
remove_from_namespace_stack(node)
|
83
79
|
end
|
84
80
|
|
85
81
|
private
|
86
82
|
|
87
|
-
|
83
|
+
#: ((Prism::ClassNode | Prism::ModuleNode) node) -> void
|
88
84
|
def add_to_namespace_stack(node)
|
89
85
|
@namespace_stack << node.constant_path.slice
|
90
86
|
end
|
91
87
|
|
92
|
-
|
88
|
+
#: ((Prism::ClassNode | Prism::ModuleNode) node) -> void
|
93
89
|
def remove_from_namespace_stack(node)
|
94
90
|
@namespace_stack.delete(node.constant_path.slice)
|
95
91
|
end
|
96
92
|
|
97
|
-
|
93
|
+
#: (Prism::CallNode node, String message) -> void
|
98
94
|
def handle_all_arg_types(node, message)
|
99
95
|
block = node.block
|
100
96
|
|
@@ -119,7 +115,9 @@ module RubyLsp
|
|
119
115
|
append_document_symbol(
|
120
116
|
name: "#{message} :#{name}",
|
121
117
|
range: range_from_location(argument.location),
|
122
|
-
selection_range: range_from_location(
|
118
|
+
selection_range: range_from_location(
|
119
|
+
argument.value_loc, #: as !nil
|
120
|
+
),
|
123
121
|
)
|
124
122
|
when Prism::StringNode
|
125
123
|
name = argument.content
|
@@ -164,7 +162,7 @@ module RubyLsp
|
|
164
162
|
end
|
165
163
|
end
|
166
164
|
|
167
|
-
|
165
|
+
#: (Prism::CallNode node, String message) -> void
|
168
166
|
def handle_symbol_and_string_arg_types(node, message)
|
169
167
|
arguments = node.arguments&.arguments
|
170
168
|
return unless arguments&.any?
|
@@ -178,7 +176,9 @@ module RubyLsp
|
|
178
176
|
append_document_symbol(
|
179
177
|
name: "#{message} :#{name}",
|
180
178
|
range: range_from_location(argument.location),
|
181
|
-
selection_range: range_from_location(
|
179
|
+
selection_range: range_from_location(
|
180
|
+
argument.value_loc, #: as !nil
|
181
|
+
),
|
182
182
|
)
|
183
183
|
when Prism::StringNode
|
184
184
|
name = argument.content
|
@@ -193,7 +193,7 @@ module RubyLsp
|
|
193
193
|
end
|
194
194
|
end
|
195
195
|
|
196
|
-
|
196
|
+
#: (Prism::CallNode node, String message) -> void
|
197
197
|
def handle_class_arg_types(node, message)
|
198
198
|
arguments = node.arguments&.arguments
|
199
199
|
return unless arguments&.any?
|
@@ -213,13 +213,7 @@ module RubyLsp
|
|
213
213
|
end
|
214
214
|
end
|
215
215
|
|
216
|
-
|
217
|
-
params(
|
218
|
-
name: String,
|
219
|
-
range: RubyLsp::Interface::Range,
|
220
|
-
selection_range: RubyLsp::Interface::Range,
|
221
|
-
).void
|
222
|
-
end
|
216
|
+
#: (name: String, range: RubyLsp::Interface::Range, selection_range: RubyLsp::Interface::Range) -> void
|
223
217
|
def append_document_symbol(name:, range:, selection_range:)
|
224
218
|
@response_builder.last.children << RubyLsp::Interface::DocumentSymbol.new(
|
225
219
|
name: name,
|
@@ -15,46 +15,37 @@ module RubyLsp
|
|
15
15
|
# # ^ hovering here will show information about the User model
|
16
16
|
# ```
|
17
17
|
class Hover
|
18
|
-
extend T::Sig
|
19
18
|
include Requests::Support::Common
|
20
19
|
|
21
|
-
|
22
|
-
params(
|
23
|
-
client: RunnerClient,
|
24
|
-
response_builder: ResponseBuilders::Hover,
|
25
|
-
node_context: NodeContext,
|
26
|
-
global_state: GlobalState,
|
27
|
-
dispatcher: Prism::Dispatcher,
|
28
|
-
).void
|
29
|
-
end
|
20
|
+
#: (RunnerClient client, ResponseBuilders::Hover response_builder, NodeContext node_context, GlobalState global_state, Prism::Dispatcher dispatcher) -> void
|
30
21
|
def initialize(client, response_builder, node_context, global_state, dispatcher)
|
31
22
|
@client = client
|
32
23
|
@response_builder = response_builder
|
33
|
-
@nesting =
|
34
|
-
@index =
|
24
|
+
@nesting = node_context.nesting #: Array[String]
|
25
|
+
@index = global_state.index #: RubyIndexer::Index
|
35
26
|
dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter)
|
36
27
|
end
|
37
28
|
|
38
|
-
|
29
|
+
#: (Prism::ConstantPathNode node) -> void
|
39
30
|
def on_constant_path_node_enter(node)
|
40
31
|
entries = @index.resolve(node.slice, @nesting)
|
41
32
|
return unless entries
|
42
33
|
|
43
|
-
name =
|
34
|
+
name = entries.first.name
|
44
35
|
generate_column_content(name)
|
45
36
|
end
|
46
37
|
|
47
|
-
|
38
|
+
#: (Prism::ConstantReadNode node) -> void
|
48
39
|
def on_constant_read_node_enter(node)
|
49
40
|
entries = @index.resolve(node.name.to_s, @nesting)
|
50
41
|
return unless entries
|
51
42
|
|
52
|
-
generate_column_content(
|
43
|
+
generate_column_content(entries.first.name)
|
53
44
|
end
|
54
45
|
|
55
46
|
private
|
56
47
|
|
57
|
-
|
48
|
+
#: (String name) -> void
|
58
49
|
def generate_column_content(name)
|
59
50
|
model = @client.model(name)
|
60
51
|
return if model.nil?
|
@@ -66,20 +57,41 @@ module RubyLsp
|
|
66
57
|
category: :documentation,
|
67
58
|
) if schema_file
|
68
59
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
60
|
+
if model[:columns].any?
|
61
|
+
@response_builder.push(
|
62
|
+
"### Columns",
|
63
|
+
category: :documentation,
|
64
|
+
)
|
65
|
+
@response_builder.push(
|
66
|
+
model[:columns].map do |name, type, default_value, nullable|
|
67
|
+
primary_key_suffix = " (PK)" if model[:primary_keys].include?(name)
|
68
|
+
foreign_key_suffix = " (FK)" if model[:foreign_keys].include?(name)
|
69
|
+
suffixes = []
|
70
|
+
suffixes << "default: #{format_default(default_value, type)}" if default_value
|
71
|
+
suffixes << "not null" unless nullable || primary_key_suffix
|
72
|
+
suffix_string = " - #{suffixes.join(" - ")}" if suffixes.any?
|
73
|
+
"- **#{name}**: #{type}#{primary_key_suffix}#{foreign_key_suffix}#{suffix_string}\n"
|
74
|
+
end.join("\n"),
|
75
|
+
category: :documentation,
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
if model[:indexes].any?
|
80
|
+
@response_builder.push(
|
81
|
+
"### Indexes",
|
82
|
+
category: :documentation,
|
83
|
+
)
|
84
|
+
@response_builder.push(
|
85
|
+
model[:indexes].map do |index|
|
86
|
+
uniqueness = index[:unique] ? " (unique)" : ""
|
87
|
+
"- **#{index[:name]}** (#{index[:columns].join(",")})#{uniqueness}"
|
88
|
+
end.join("\n"),
|
89
|
+
category: :documentation,
|
90
|
+
)
|
91
|
+
end
|
80
92
|
end
|
81
93
|
|
82
|
-
|
94
|
+
#: (String default_value, String type) -> String
|
83
95
|
def format_default(default_value, type)
|
84
96
|
case type
|
85
97
|
when "boolean"
|
@@ -4,13 +4,8 @@
|
|
4
4
|
module RubyLsp
|
5
5
|
module Rails
|
6
6
|
class IndexingEnhancement < RubyIndexer::Enhancement
|
7
|
-
|
8
|
-
|
9
|
-
sig do
|
10
|
-
override.params(
|
11
|
-
call_node: Prism::CallNode,
|
12
|
-
).void
|
13
|
-
end
|
7
|
+
# @override
|
8
|
+
#: (Prism::CallNode call_node) -> void
|
14
9
|
def on_call_node_enter(call_node)
|
15
10
|
owner = @listener.current_owner
|
16
11
|
return unless owner
|
@@ -26,11 +21,8 @@ module RubyLsp
|
|
26
21
|
end
|
27
22
|
end
|
28
23
|
|
29
|
-
|
30
|
-
|
31
|
-
call_node: Prism::CallNode,
|
32
|
-
).void
|
33
|
-
end
|
24
|
+
# @override
|
25
|
+
#: (Prism::CallNode call_node) -> void
|
34
26
|
def on_call_node_leave(call_node)
|
35
27
|
if call_node.name == :class_methods && call_node.block
|
36
28
|
@listener.pop_namespace_stack
|
@@ -39,12 +31,7 @@ module RubyLsp
|
|
39
31
|
|
40
32
|
private
|
41
33
|
|
42
|
-
|
43
|
-
params(
|
44
|
-
owner: RubyIndexer::Entry::Namespace,
|
45
|
-
call_node: Prism::CallNode,
|
46
|
-
).void
|
47
|
-
end
|
34
|
+
#: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
|
48
35
|
def handle_association(owner, call_node)
|
49
36
|
arguments = call_node.arguments&.arguments
|
50
37
|
return unless arguments
|
@@ -73,7 +60,7 @@ module RubyLsp
|
|
73
60
|
@listener.add_method("#{name}=", loc, writer_signatures)
|
74
61
|
end
|
75
62
|
|
76
|
-
|
63
|
+
#: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
|
77
64
|
def handle_concern_extend(owner, call_node)
|
78
65
|
arguments = call_node.arguments&.arguments
|
79
66
|
return unless arguments
|
@@ -98,7 +85,7 @@ module RubyLsp
|
|
98
85
|
end
|
99
86
|
end
|
100
87
|
|
101
|
-
|
88
|
+
#: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
|
102
89
|
def handle_class_methods(owner, call_node)
|
103
90
|
return unless call_node.block
|
104
91
|
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
module Rails
|
6
|
+
class RailsTestStyle < Listeners::TestDiscovery
|
7
|
+
BASE_COMMAND = "#{RbConfig.ruby} bin/rails test" #: String
|
8
|
+
|
9
|
+
class << self
|
10
|
+
#: (Array[Hash[Symbol, untyped]]) -> Array[String]
|
11
|
+
def resolve_test_commands(items)
|
12
|
+
commands = []
|
13
|
+
queue = items.dup
|
14
|
+
|
15
|
+
full_files = []
|
16
|
+
|
17
|
+
until queue.empty?
|
18
|
+
item = queue.shift #: as !nil
|
19
|
+
tags = Set.new(item[:tags])
|
20
|
+
next unless tags.include?("framework:rails")
|
21
|
+
|
22
|
+
children = item[:children]
|
23
|
+
uri = URI(item[:uri])
|
24
|
+
path = uri.full_path
|
25
|
+
next unless path
|
26
|
+
|
27
|
+
if tags.include?("test_dir")
|
28
|
+
if children.empty?
|
29
|
+
full_files.concat(Dir.glob(
|
30
|
+
"#{path}/**/{*_test,test_*}.rb",
|
31
|
+
File::Constants::FNM_EXTGLOB | File::Constants::FNM_PATHNAME,
|
32
|
+
))
|
33
|
+
end
|
34
|
+
elsif tags.include?("test_file")
|
35
|
+
full_files << path if children.empty?
|
36
|
+
elsif tags.include?("test_group")
|
37
|
+
commands << "#{BASE_COMMAND} #{path} --name \"/#{Shellwords.escape(item[:id])}(#|::)/\""
|
38
|
+
else
|
39
|
+
full_files << "#{path}:#{item.dig(:range, :start, :line) + 1}"
|
40
|
+
end
|
41
|
+
|
42
|
+
queue.concat(children)
|
43
|
+
end
|
44
|
+
|
45
|
+
unless full_files.empty?
|
46
|
+
commands << "#{BASE_COMMAND} #{full_files.join(" ")}"
|
47
|
+
end
|
48
|
+
|
49
|
+
commands
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#: (RunnerClient client, ResponseBuilders::TestCollection response_builder, GlobalState global_state, Prism::Dispatcher dispatcher, URI::Generic uri) -> void
|
54
|
+
def initialize(client, response_builder, global_state, dispatcher, uri)
|
55
|
+
super(response_builder, global_state, dispatcher, uri)
|
56
|
+
|
57
|
+
dispatcher.register(
|
58
|
+
self,
|
59
|
+
:on_class_node_enter,
|
60
|
+
:on_call_node_enter,
|
61
|
+
:on_def_node_enter,
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
#: (Prism::ClassNode node) -> void
|
66
|
+
def on_class_node_enter(node)
|
67
|
+
with_test_ancestor_tracking(node) do |name, ancestors|
|
68
|
+
if declarative_minitest?(ancestors, name)
|
69
|
+
test_item = Requests::Support::TestItem.new(
|
70
|
+
name,
|
71
|
+
name,
|
72
|
+
@uri,
|
73
|
+
range_from_node(node),
|
74
|
+
framework: :rails,
|
75
|
+
)
|
76
|
+
|
77
|
+
@response_builder.add(test_item)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
#: (Prism::CallNode node) -> void
|
83
|
+
def on_call_node_enter(node)
|
84
|
+
return unless node.name == :test
|
85
|
+
return unless node.block
|
86
|
+
|
87
|
+
arguments = node.arguments&.arguments
|
88
|
+
first_arg = arguments&.first
|
89
|
+
return unless first_arg.is_a?(Prism::StringNode)
|
90
|
+
|
91
|
+
test_name = first_arg.unescaped
|
92
|
+
test_name = "<empty test name>" if test_name.empty?
|
93
|
+
|
94
|
+
# Rails' `test "foo bar"` helper defines a method `def test_foo_bar`. We normalize test names
|
95
|
+
# the same way (spaces to underscores, prefix with `test_`) to match the actual method names
|
96
|
+
# Rails uses at runtime, ensuring proper test discovery and execution.
|
97
|
+
rails_normalized_name = "test_#{test_name.gsub(/\s+/, "_")}"
|
98
|
+
|
99
|
+
add_test_item(node, rails_normalized_name)
|
100
|
+
end
|
101
|
+
|
102
|
+
#: (Prism::DefNode node) -> void
|
103
|
+
def on_def_node_enter(node)
|
104
|
+
return if @visibility_stack.last != :public
|
105
|
+
|
106
|
+
name = node.name.to_s
|
107
|
+
return unless name.start_with?("test_")
|
108
|
+
|
109
|
+
add_test_item(node, name)
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
#: (Array[String] attached_ancestors, String fully_qualified_name) -> bool
|
115
|
+
def declarative_minitest?(attached_ancestors, fully_qualified_name)
|
116
|
+
# The declarative test style is present as long as the class extends
|
117
|
+
# ActiveSupport::Testing::Declarative
|
118
|
+
name_parts = fully_qualified_name.split("::")
|
119
|
+
singleton_name = "#{name_parts.join("::")}::<Class:#{name_parts.last}>"
|
120
|
+
@index.linearized_ancestors_of(singleton_name).include?("ActiveSupport::Testing::Declarative")
|
121
|
+
rescue RubyIndexer::Index::NonExistingNamespaceError
|
122
|
+
false
|
123
|
+
end
|
124
|
+
|
125
|
+
#: (Prism::Node node, String test_name) -> void
|
126
|
+
def add_test_item(node, test_name)
|
127
|
+
test_item = group_test_item
|
128
|
+
return unless test_item
|
129
|
+
|
130
|
+
test_item.add(Requests::Support::TestItem.new(
|
131
|
+
"#{test_item.id}##{test_name}",
|
132
|
+
test_name,
|
133
|
+
@uri,
|
134
|
+
range_from_node(node),
|
135
|
+
framework: :rails,
|
136
|
+
))
|
137
|
+
end
|
138
|
+
|
139
|
+
#: -> Requests::Support::TestItem?
|
140
|
+
def group_test_item
|
141
|
+
current_group_name = RubyIndexer::Index.actual_nesting(@nesting, nil).join("::")
|
142
|
+
|
143
|
+
# If we're finding a test method, but for the wrong framework, then the group test item will not have been
|
144
|
+
# previously pushed and thus we return early and avoid adding items for a framework this listener is not
|
145
|
+
# interested in
|
146
|
+
@response_builder[current_group_name]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|