ruby-lsp 0.16.6 → 0.17.0
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 -2
- data/VERSION +1 -1
- data/exe/ruby-lsp +21 -4
- data/exe/ruby-lsp-check +1 -3
- data/exe/ruby-lsp-doctor +1 -4
- data/lib/core_ext/uri.rb +3 -0
- data/lib/ruby_indexer/lib/ruby_indexer/{collector.rb → declaration_listener.rb} +258 -140
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +101 -12
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +187 -12
- data/lib/ruby_indexer/ruby_indexer.rb +1 -1
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +106 -10
- data/lib/ruby_indexer/test/configuration_test.rb +4 -5
- data/lib/ruby_indexer/test/constant_test.rb +11 -8
- data/lib/ruby_indexer/test/index_test.rb +528 -0
- data/lib/ruby_indexer/test/instance_variables_test.rb +131 -0
- data/lib/ruby_indexer/test/method_test.rb +93 -0
- data/lib/ruby_indexer/test/test_case.rb +3 -1
- data/lib/ruby_lsp/addon.rb +8 -8
- data/lib/ruby_lsp/document.rb +3 -3
- data/lib/ruby_lsp/internal.rb +1 -0
- data/lib/ruby_lsp/listeners/code_lens.rb +11 -0
- data/lib/ruby_lsp/listeners/completion.rb +144 -51
- data/lib/ruby_lsp/listeners/definition.rb +77 -12
- data/lib/ruby_lsp/listeners/document_highlight.rb +1 -1
- data/lib/ruby_lsp/listeners/document_link.rb +1 -1
- data/lib/ruby_lsp/listeners/hover.rb +60 -6
- data/lib/ruby_lsp/listeners/semantic_highlighting.rb +59 -3
- data/lib/ruby_lsp/listeners/signature_help.rb +4 -4
- data/lib/ruby_lsp/node_context.rb +28 -0
- data/lib/ruby_lsp/requests/code_action_resolve.rb +73 -2
- data/lib/ruby_lsp/requests/code_actions.rb +16 -15
- data/lib/ruby_lsp/requests/completion.rb +22 -13
- data/lib/ruby_lsp/requests/completion_resolve.rb +26 -10
- data/lib/ruby_lsp/requests/definition.rb +21 -5
- data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
- data/lib/ruby_lsp/requests/hover.rb +5 -6
- data/lib/ruby_lsp/requests/on_type_formatting.rb +8 -4
- data/lib/ruby_lsp/requests/signature_help.rb +3 -3
- data/lib/ruby_lsp/requests/support/common.rb +20 -1
- data/lib/ruby_lsp/requests/workspace_symbol.rb +3 -1
- data/lib/ruby_lsp/server.rb +10 -4
- metadata +10 -8
@@ -14,24 +14,31 @@ module RubyLsp
|
|
14
14
|
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
|
15
15
|
global_state: GlobalState,
|
16
16
|
uri: URI::Generic,
|
17
|
-
|
17
|
+
node_context: NodeContext,
|
18
18
|
dispatcher: Prism::Dispatcher,
|
19
19
|
typechecker_enabled: T::Boolean,
|
20
20
|
).void
|
21
21
|
end
|
22
|
-
def initialize(response_builder, global_state, uri,
|
22
|
+
def initialize(response_builder, global_state, uri, node_context, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
|
23
23
|
@response_builder = response_builder
|
24
24
|
@global_state = global_state
|
25
25
|
@index = T.let(global_state.index, RubyIndexer::Index)
|
26
26
|
@uri = uri
|
27
|
-
@
|
27
|
+
@node_context = node_context
|
28
28
|
@typechecker_enabled = typechecker_enabled
|
29
29
|
|
30
30
|
dispatcher.register(
|
31
31
|
self,
|
32
32
|
:on_call_node_enter,
|
33
|
+
:on_block_argument_node_enter,
|
33
34
|
:on_constant_read_node_enter,
|
34
35
|
:on_constant_path_node_enter,
|
36
|
+
:on_instance_variable_read_node_enter,
|
37
|
+
:on_instance_variable_write_node_enter,
|
38
|
+
:on_instance_variable_and_write_node_enter,
|
39
|
+
:on_instance_variable_operator_write_node_enter,
|
40
|
+
:on_instance_variable_or_write_node_enter,
|
41
|
+
:on_instance_variable_target_node_enter,
|
35
42
|
)
|
36
43
|
end
|
37
44
|
|
@@ -42,10 +49,21 @@ module RubyLsp
|
|
42
49
|
if message == :require || message == :require_relative
|
43
50
|
handle_require_definition(node)
|
44
51
|
else
|
45
|
-
handle_method_definition(node)
|
52
|
+
handle_method_definition(message.to_s, self_receiver?(node))
|
46
53
|
end
|
47
54
|
end
|
48
55
|
|
56
|
+
sig { params(node: Prism::BlockArgumentNode).void }
|
57
|
+
def on_block_argument_node_enter(node)
|
58
|
+
expression = node.expression
|
59
|
+
return unless expression.is_a?(Prism::SymbolNode)
|
60
|
+
|
61
|
+
value = expression.value
|
62
|
+
return unless value
|
63
|
+
|
64
|
+
handle_method_definition(value, false)
|
65
|
+
end
|
66
|
+
|
49
67
|
sig { params(node: Prism::ConstantPathNode).void }
|
50
68
|
def on_constant_path_node_enter(node)
|
51
69
|
name = constant_name(node)
|
@@ -62,15 +80,62 @@ module RubyLsp
|
|
62
80
|
find_in_index(name)
|
63
81
|
end
|
64
82
|
|
83
|
+
sig { params(node: Prism::InstanceVariableReadNode).void }
|
84
|
+
def on_instance_variable_read_node_enter(node)
|
85
|
+
handle_instance_variable_definition(node.name.to_s)
|
86
|
+
end
|
87
|
+
|
88
|
+
sig { params(node: Prism::InstanceVariableWriteNode).void }
|
89
|
+
def on_instance_variable_write_node_enter(node)
|
90
|
+
handle_instance_variable_definition(node.name.to_s)
|
91
|
+
end
|
92
|
+
|
93
|
+
sig { params(node: Prism::InstanceVariableAndWriteNode).void }
|
94
|
+
def on_instance_variable_and_write_node_enter(node)
|
95
|
+
handle_instance_variable_definition(node.name.to_s)
|
96
|
+
end
|
97
|
+
|
98
|
+
sig { params(node: Prism::InstanceVariableOperatorWriteNode).void }
|
99
|
+
def on_instance_variable_operator_write_node_enter(node)
|
100
|
+
handle_instance_variable_definition(node.name.to_s)
|
101
|
+
end
|
102
|
+
|
103
|
+
sig { params(node: Prism::InstanceVariableOrWriteNode).void }
|
104
|
+
def on_instance_variable_or_write_node_enter(node)
|
105
|
+
handle_instance_variable_definition(node.name.to_s)
|
106
|
+
end
|
107
|
+
|
108
|
+
sig { params(node: Prism::InstanceVariableTargetNode).void }
|
109
|
+
def on_instance_variable_target_node_enter(node)
|
110
|
+
handle_instance_variable_definition(node.name.to_s)
|
111
|
+
end
|
112
|
+
|
65
113
|
private
|
66
114
|
|
67
|
-
sig { params(
|
68
|
-
def
|
69
|
-
|
70
|
-
return unless
|
115
|
+
sig { params(name: String).void }
|
116
|
+
def handle_instance_variable_definition(name)
|
117
|
+
entries = @index.resolve_instance_variable(name, @node_context.fully_qualified_name)
|
118
|
+
return unless entries
|
119
|
+
|
120
|
+
entries.each do |entry|
|
121
|
+
location = entry.location
|
122
|
+
|
123
|
+
@response_builder << Interface::Location.new(
|
124
|
+
uri: URI::Generic.from_path(path: entry.file_path).to_s,
|
125
|
+
range: Interface::Range.new(
|
126
|
+
start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
|
127
|
+
end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
|
128
|
+
),
|
129
|
+
)
|
130
|
+
end
|
131
|
+
rescue RubyIndexer::Index::NonExistingNamespaceError
|
132
|
+
# If by any chance we haven't indexed the owner, then there's no way to find the right declaration
|
133
|
+
end
|
71
134
|
|
72
|
-
|
73
|
-
|
135
|
+
sig { params(message: String, self_receiver: T::Boolean).void }
|
136
|
+
def handle_method_definition(message, self_receiver)
|
137
|
+
methods = if self_receiver
|
138
|
+
@index.resolve_method(message, @node_context.fully_qualified_name)
|
74
139
|
else
|
75
140
|
# If the method doesn't have a receiver, then we provide a few candidates to jump to
|
76
141
|
# But we don't want to provide too many candidates, as it can be overwhelming
|
@@ -138,13 +203,13 @@ module RubyLsp
|
|
138
203
|
|
139
204
|
sig { params(value: String).void }
|
140
205
|
def find_in_index(value)
|
141
|
-
entries = @index.resolve(value, @nesting)
|
206
|
+
entries = @index.resolve(value, @node_context.nesting)
|
142
207
|
return unless entries
|
143
208
|
|
144
209
|
# We should only allow jumping to the definition of private constants if the constant is defined in the same
|
145
210
|
# namespace as the reference
|
146
211
|
first_entry = T.must(entries.first)
|
147
|
-
return if first_entry.
|
212
|
+
return if first_entry.private? && first_entry.name != "#{@node_context.fully_qualified_name}::#{value}"
|
148
213
|
|
149
214
|
entries.each do |entry|
|
150
215
|
location = entry.location
|
@@ -271,7 +271,7 @@ module RubyLsp
|
|
271
271
|
def on_constant_path_node_enter(node)
|
272
272
|
return unless matches?(node, CONSTANT_PATH_NODES)
|
273
273
|
|
274
|
-
add_highlight(Constant::DocumentHighlightKind::READ, node.
|
274
|
+
add_highlight(Constant::DocumentHighlightKind::READ, node.name_loc)
|
275
275
|
end
|
276
276
|
|
277
277
|
sig { params(node: Prism::ConstantReadNode).void }
|
@@ -30,7 +30,7 @@ module RubyLsp
|
|
30
30
|
lookup[spec.name] = {}
|
31
31
|
lookup[spec.name][spec.version.to_s] = {}
|
32
32
|
|
33
|
-
Dir.glob("**/*.rb", base: "#{spec.full_gem_path}/").each do |path|
|
33
|
+
Dir.glob("**/*.rb", base: "#{spec.full_gem_path.delete_prefix("//?/")}/").each do |path|
|
34
34
|
lookup[spec.name][spec.version.to_s][path] = "#{spec.full_gem_path}/#{path}"
|
35
35
|
end
|
36
36
|
end
|
@@ -13,6 +13,12 @@ module RubyLsp
|
|
13
13
|
Prism::ConstantReadNode,
|
14
14
|
Prism::ConstantWriteNode,
|
15
15
|
Prism::ConstantPathNode,
|
16
|
+
Prism::InstanceVariableReadNode,
|
17
|
+
Prism::InstanceVariableAndWriteNode,
|
18
|
+
Prism::InstanceVariableOperatorWriteNode,
|
19
|
+
Prism::InstanceVariableOrWriteNode,
|
20
|
+
Prism::InstanceVariableTargetNode,
|
21
|
+
Prism::InstanceVariableWriteNode,
|
16
22
|
],
|
17
23
|
T::Array[T.class_of(Prism::Node)],
|
18
24
|
)
|
@@ -30,17 +36,17 @@ module RubyLsp
|
|
30
36
|
response_builder: ResponseBuilders::Hover,
|
31
37
|
global_state: GlobalState,
|
32
38
|
uri: URI::Generic,
|
33
|
-
|
39
|
+
node_context: NodeContext,
|
34
40
|
dispatcher: Prism::Dispatcher,
|
35
41
|
typechecker_enabled: T::Boolean,
|
36
42
|
).void
|
37
43
|
end
|
38
|
-
def initialize(response_builder, global_state, uri,
|
44
|
+
def initialize(response_builder, global_state, uri, node_context, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
|
39
45
|
@response_builder = response_builder
|
40
46
|
@global_state = global_state
|
41
47
|
@index = T.let(global_state.index, RubyIndexer::Index)
|
42
48
|
@path = T.let(uri.to_standardized_path, T.nilable(String))
|
43
|
-
@
|
49
|
+
@node_context = node_context
|
44
50
|
@typechecker_enabled = typechecker_enabled
|
45
51
|
|
46
52
|
dispatcher.register(
|
@@ -49,6 +55,12 @@ module RubyLsp
|
|
49
55
|
:on_constant_write_node_enter,
|
50
56
|
:on_constant_path_node_enter,
|
51
57
|
:on_call_node_enter,
|
58
|
+
:on_instance_variable_read_node_enter,
|
59
|
+
:on_instance_variable_write_node_enter,
|
60
|
+
:on_instance_variable_and_write_node_enter,
|
61
|
+
:on_instance_variable_operator_write_node_enter,
|
62
|
+
:on_instance_variable_or_write_node_enter,
|
63
|
+
:on_instance_variable_target_node_enter,
|
52
64
|
)
|
53
65
|
end
|
54
66
|
|
@@ -93,7 +105,7 @@ module RubyLsp
|
|
93
105
|
message = node.message
|
94
106
|
return unless message
|
95
107
|
|
96
|
-
methods = @index.resolve_method(message, @
|
108
|
+
methods = @index.resolve_method(message, @node_context.fully_qualified_name)
|
97
109
|
return unless methods
|
98
110
|
|
99
111
|
categorized_markdown_from_index_entries(message, methods).each do |category, content|
|
@@ -101,17 +113,59 @@ module RubyLsp
|
|
101
113
|
end
|
102
114
|
end
|
103
115
|
|
116
|
+
sig { params(node: Prism::InstanceVariableReadNode).void }
|
117
|
+
def on_instance_variable_read_node_enter(node)
|
118
|
+
handle_instance_variable_hover(node.name.to_s)
|
119
|
+
end
|
120
|
+
|
121
|
+
sig { params(node: Prism::InstanceVariableWriteNode).void }
|
122
|
+
def on_instance_variable_write_node_enter(node)
|
123
|
+
handle_instance_variable_hover(node.name.to_s)
|
124
|
+
end
|
125
|
+
|
126
|
+
sig { params(node: Prism::InstanceVariableAndWriteNode).void }
|
127
|
+
def on_instance_variable_and_write_node_enter(node)
|
128
|
+
handle_instance_variable_hover(node.name.to_s)
|
129
|
+
end
|
130
|
+
|
131
|
+
sig { params(node: Prism::InstanceVariableOperatorWriteNode).void }
|
132
|
+
def on_instance_variable_operator_write_node_enter(node)
|
133
|
+
handle_instance_variable_hover(node.name.to_s)
|
134
|
+
end
|
135
|
+
|
136
|
+
sig { params(node: Prism::InstanceVariableOrWriteNode).void }
|
137
|
+
def on_instance_variable_or_write_node_enter(node)
|
138
|
+
handle_instance_variable_hover(node.name.to_s)
|
139
|
+
end
|
140
|
+
|
141
|
+
sig { params(node: Prism::InstanceVariableTargetNode).void }
|
142
|
+
def on_instance_variable_target_node_enter(node)
|
143
|
+
handle_instance_variable_hover(node.name.to_s)
|
144
|
+
end
|
145
|
+
|
104
146
|
private
|
105
147
|
|
148
|
+
sig { params(name: String).void }
|
149
|
+
def handle_instance_variable_hover(name)
|
150
|
+
entries = @index.resolve_instance_variable(name, @node_context.fully_qualified_name)
|
151
|
+
return unless entries
|
152
|
+
|
153
|
+
categorized_markdown_from_index_entries(name, entries).each do |category, content|
|
154
|
+
@response_builder.push(content, category: category)
|
155
|
+
end
|
156
|
+
rescue RubyIndexer::Index::NonExistingNamespaceError
|
157
|
+
# If by any chance we haven't indexed the owner, then there's no way to find the right declaration
|
158
|
+
end
|
159
|
+
|
106
160
|
sig { params(name: String, location: Prism::Location).void }
|
107
161
|
def generate_hover(name, location)
|
108
|
-
entries = @index.resolve(name, @nesting)
|
162
|
+
entries = @index.resolve(name, @node_context.nesting)
|
109
163
|
return unless entries
|
110
164
|
|
111
165
|
# We should only show hover for private constants if the constant is defined in the same namespace as the
|
112
166
|
# reference
|
113
167
|
first_entry = T.must(entries.first)
|
114
|
-
return if first_entry.
|
168
|
+
return if first_entry.private? && first_entry.name != "#{@node_context.fully_qualified_name}::#{name}"
|
115
169
|
|
116
170
|
categorized_markdown_from_index_entries(name, entries).each do |category, content|
|
117
171
|
@response_builder.push(content, category: category)
|
@@ -58,6 +58,7 @@ module RubyLsp
|
|
58
58
|
:on_constant_operator_write_node_enter,
|
59
59
|
:on_constant_or_write_node_enter,
|
60
60
|
:on_constant_target_node_enter,
|
61
|
+
:on_constant_path_node_enter,
|
61
62
|
:on_local_variable_and_write_node_enter,
|
62
63
|
:on_local_variable_operator_write_node_enter,
|
63
64
|
:on_local_variable_or_write_node_enter,
|
@@ -302,17 +303,64 @@ module RubyLsp
|
|
302
303
|
def on_class_node_enter(node)
|
303
304
|
return unless visible?(node, @range)
|
304
305
|
|
305
|
-
|
306
|
+
constant_path = node.constant_path
|
307
|
+
|
308
|
+
if constant_path.is_a?(Prism::ConstantReadNode)
|
309
|
+
@response_builder.add_token(constant_path.location, :class, [:declaration])
|
310
|
+
else
|
311
|
+
each_constant_path_part(constant_path) do |part|
|
312
|
+
loc = case part
|
313
|
+
when Prism::ConstantPathNode
|
314
|
+
part.name_loc
|
315
|
+
when Prism::ConstantReadNode
|
316
|
+
part.location
|
317
|
+
end
|
318
|
+
next unless loc
|
319
|
+
|
320
|
+
@response_builder.add_token(loc, :class, [:declaration])
|
321
|
+
end
|
322
|
+
end
|
306
323
|
|
307
324
|
superclass = node.superclass
|
308
|
-
|
325
|
+
|
326
|
+
if superclass.is_a?(Prism::ConstantReadNode)
|
327
|
+
@response_builder.add_token(superclass.location, :class)
|
328
|
+
elsif superclass
|
329
|
+
each_constant_path_part(superclass) do |part|
|
330
|
+
loc = case part
|
331
|
+
when Prism::ConstantPathNode
|
332
|
+
part.name_loc
|
333
|
+
when Prism::ConstantReadNode
|
334
|
+
part.location
|
335
|
+
end
|
336
|
+
next unless loc
|
337
|
+
|
338
|
+
@response_builder.add_token(loc, :class)
|
339
|
+
end
|
340
|
+
end
|
309
341
|
end
|
310
342
|
|
311
343
|
sig { params(node: Prism::ModuleNode).void }
|
312
344
|
def on_module_node_enter(node)
|
313
345
|
return unless visible?(node, @range)
|
314
346
|
|
315
|
-
|
347
|
+
constant_path = node.constant_path
|
348
|
+
|
349
|
+
if constant_path.is_a?(Prism::ConstantReadNode)
|
350
|
+
@response_builder.add_token(constant_path.location, :namespace, [:declaration])
|
351
|
+
else
|
352
|
+
each_constant_path_part(constant_path) do |part|
|
353
|
+
loc = case part
|
354
|
+
when Prism::ConstantPathNode
|
355
|
+
part.name_loc
|
356
|
+
when Prism::ConstantReadNode
|
357
|
+
part.location
|
358
|
+
end
|
359
|
+
next unless loc
|
360
|
+
|
361
|
+
@response_builder.add_token(loc, :namespace, [:declaration])
|
362
|
+
end
|
363
|
+
end
|
316
364
|
end
|
317
365
|
|
318
366
|
sig { params(node: Prism::ImplicitNode).void }
|
@@ -327,6 +375,14 @@ module RubyLsp
|
|
327
375
|
@inside_implicit_node = false
|
328
376
|
end
|
329
377
|
|
378
|
+
sig { params(node: Prism::ConstantPathNode).void }
|
379
|
+
def on_constant_path_node_enter(node)
|
380
|
+
return if @inside_implicit_node
|
381
|
+
return unless visible?(node, @range)
|
382
|
+
|
383
|
+
@response_builder.add_token(node.name_loc, :namespace)
|
384
|
+
end
|
385
|
+
|
330
386
|
private
|
331
387
|
|
332
388
|
# Textmate provides highlighting for a subset of these special Ruby-specific methods. We want to utilize that
|
@@ -11,17 +11,17 @@ module RubyLsp
|
|
11
11
|
params(
|
12
12
|
response_builder: ResponseBuilders::SignatureHelp,
|
13
13
|
global_state: GlobalState,
|
14
|
-
|
14
|
+
node_context: NodeContext,
|
15
15
|
dispatcher: Prism::Dispatcher,
|
16
16
|
typechecker_enabled: T::Boolean,
|
17
17
|
).void
|
18
18
|
end
|
19
|
-
def initialize(response_builder, global_state,
|
19
|
+
def initialize(response_builder, global_state, node_context, dispatcher, typechecker_enabled)
|
20
20
|
@typechecker_enabled = typechecker_enabled
|
21
21
|
@response_builder = response_builder
|
22
22
|
@global_state = global_state
|
23
23
|
@index = T.let(global_state.index, RubyIndexer::Index)
|
24
|
-
@
|
24
|
+
@node_context = node_context
|
25
25
|
dispatcher.register(self, :on_call_node_enter)
|
26
26
|
end
|
27
27
|
|
@@ -33,7 +33,7 @@ module RubyLsp
|
|
33
33
|
message = node.message
|
34
34
|
return unless message
|
35
35
|
|
36
|
-
methods = @index.resolve_method(message, @
|
36
|
+
methods = @index.resolve_method(message, @node_context.fully_qualified_name)
|
37
37
|
return unless methods
|
38
38
|
|
39
39
|
target_method = methods.first
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
# This class allows listeners to access contextual information about a node in the AST, such as its parent
|
6
|
+
# and its namespace nesting.
|
7
|
+
class NodeContext
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { returns(T.nilable(Prism::Node)) }
|
11
|
+
attr_reader :node, :parent
|
12
|
+
|
13
|
+
sig { returns(T::Array[String]) }
|
14
|
+
attr_reader :nesting
|
15
|
+
|
16
|
+
sig { params(node: T.nilable(Prism::Node), parent: T.nilable(Prism::Node), nesting: T::Array[String]).void }
|
17
|
+
def initialize(node, parent, nesting)
|
18
|
+
@node = node
|
19
|
+
@parent = parent
|
20
|
+
@nesting = nesting
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { returns(String) }
|
24
|
+
def fully_qualified_name
|
25
|
+
@fully_qualified_name ||= T.let(@nesting.join("::"), T.nilable(String))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -24,6 +24,7 @@ module RubyLsp
|
|
24
24
|
class CodeActionResolve < Request
|
25
25
|
extend T::Sig
|
26
26
|
NEW_VARIABLE_NAME = "new_variable"
|
27
|
+
NEW_METHOD_NAME = "new_method"
|
27
28
|
|
28
29
|
class CodeActionError < StandardError; end
|
29
30
|
|
@@ -31,6 +32,7 @@ module RubyLsp
|
|
31
32
|
enums do
|
32
33
|
EmptySelection = new
|
33
34
|
InvalidTargetRange = new
|
35
|
+
UnknownCodeAction = new
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
@@ -43,6 +45,18 @@ module RubyLsp
|
|
43
45
|
|
44
46
|
sig { override.returns(T.any(Interface::CodeAction, Error)) }
|
45
47
|
def perform
|
48
|
+
case @code_action[:title]
|
49
|
+
when CodeActions::EXTRACT_TO_VARIABLE_TITLE
|
50
|
+
refactor_variable
|
51
|
+
when CodeActions::EXTRACT_TO_METHOD_TITLE
|
52
|
+
refactor_method
|
53
|
+
else
|
54
|
+
Error::UnknownCodeAction
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
sig { returns(T.any(Interface::CodeAction, Error)) }
|
59
|
+
def refactor_variable
|
46
60
|
return Error::EmptySelection if @document.source.empty?
|
47
61
|
|
48
62
|
source_range = @code_action.dig(:data, :range)
|
@@ -54,9 +68,11 @@ module RubyLsp
|
|
54
68
|
extracted_source = T.must(@document.source[start_index...end_index])
|
55
69
|
|
56
70
|
# Find the closest statements node, so that we place the refactor in a valid position
|
57
|
-
|
71
|
+
node_context = @document
|
58
72
|
.locate(@document.tree, start_index, node_types: [Prism::StatementsNode, Prism::BlockNode])
|
59
73
|
|
74
|
+
closest_statements = node_context.node
|
75
|
+
parent_statements = node_context.parent
|
60
76
|
return Error::InvalidTargetRange if closest_statements.nil? || closest_statements.child_nodes.compact.empty?
|
61
77
|
|
62
78
|
# Find the node with the end line closest to the requested position, so that we can place the refactor
|
@@ -117,7 +133,7 @@ module RubyLsp
|
|
117
133
|
end
|
118
134
|
|
119
135
|
Interface::CodeAction.new(
|
120
|
-
title:
|
136
|
+
title: CodeActions::EXTRACT_TO_VARIABLE_TITLE,
|
121
137
|
edit: Interface::WorkspaceEdit.new(
|
122
138
|
document_changes: [
|
123
139
|
Interface::TextDocumentEdit.new(
|
@@ -135,6 +151,61 @@ module RubyLsp
|
|
135
151
|
)
|
136
152
|
end
|
137
153
|
|
154
|
+
sig { returns(T.any(Interface::CodeAction, Error)) }
|
155
|
+
def refactor_method
|
156
|
+
return Error::EmptySelection if @document.source.empty?
|
157
|
+
|
158
|
+
source_range = @code_action.dig(:data, :range)
|
159
|
+
return Error::EmptySelection if source_range[:start] == source_range[:end]
|
160
|
+
|
161
|
+
scanner = @document.create_scanner
|
162
|
+
start_index = scanner.find_char_position(source_range[:start])
|
163
|
+
end_index = scanner.find_char_position(source_range[:end])
|
164
|
+
extracted_source = T.must(@document.source[start_index...end_index])
|
165
|
+
|
166
|
+
# Find the closest method declaration node, so that we place the refactor in a valid position
|
167
|
+
node_context = @document.locate(@document.tree, start_index, node_types: [Prism::DefNode])
|
168
|
+
closest_def = T.cast(node_context.node, Prism::DefNode)
|
169
|
+
return Error::InvalidTargetRange if closest_def.nil?
|
170
|
+
|
171
|
+
end_keyword_loc = closest_def.end_keyword_loc
|
172
|
+
return Error::InvalidTargetRange if end_keyword_loc.nil?
|
173
|
+
|
174
|
+
end_line = end_keyword_loc.end_line - 1
|
175
|
+
character = end_keyword_loc.end_column
|
176
|
+
indentation = " " * end_keyword_loc.start_column
|
177
|
+
target_range = {
|
178
|
+
start: { line: end_line, character: character },
|
179
|
+
end: { line: end_line, character: character },
|
180
|
+
}
|
181
|
+
|
182
|
+
new_method_source = <<~RUBY.chomp
|
183
|
+
|
184
|
+
|
185
|
+
#{indentation}def #{NEW_METHOD_NAME}
|
186
|
+
#{indentation} #{extracted_source}
|
187
|
+
#{indentation}end
|
188
|
+
RUBY
|
189
|
+
|
190
|
+
Interface::CodeAction.new(
|
191
|
+
title: CodeActions::EXTRACT_TO_METHOD_TITLE,
|
192
|
+
edit: Interface::WorkspaceEdit.new(
|
193
|
+
document_changes: [
|
194
|
+
Interface::TextDocumentEdit.new(
|
195
|
+
text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
|
196
|
+
uri: @code_action.dig(:data, :uri),
|
197
|
+
version: nil,
|
198
|
+
),
|
199
|
+
edits: [
|
200
|
+
create_text_edit(target_range, new_method_source),
|
201
|
+
create_text_edit(source_range, NEW_METHOD_NAME),
|
202
|
+
],
|
203
|
+
),
|
204
|
+
],
|
205
|
+
),
|
206
|
+
)
|
207
|
+
end
|
208
|
+
|
138
209
|
private
|
139
210
|
|
140
211
|
sig { params(range: T::Hash[Symbol, T.untyped], new_text: String).returns(Interface::TextEdit) }
|
@@ -19,6 +19,9 @@ module RubyLsp
|
|
19
19
|
class CodeActions < Request
|
20
20
|
extend T::Sig
|
21
21
|
|
22
|
+
EXTRACT_TO_VARIABLE_TITLE = "Refactor: Extract Variable"
|
23
|
+
EXTRACT_TO_METHOD_TITLE = "Refactor: Extract Method"
|
24
|
+
|
22
25
|
class << self
|
23
26
|
extend T::Sig
|
24
27
|
|
@@ -52,22 +55,20 @@ module RubyLsp
|
|
52
55
|
end
|
53
56
|
|
54
57
|
# Only add refactor actions if there's a non empty selection in the editor
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
unless @range.dig(:start) == @range.dig(:end)
|
59
|
+
code_actions << Interface::CodeAction.new(
|
60
|
+
title: EXTRACT_TO_VARIABLE_TITLE,
|
61
|
+
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
62
|
+
data: { range: @range, uri: @uri.to_s },
|
63
|
+
)
|
64
|
+
code_actions << Interface::CodeAction.new(
|
65
|
+
title: EXTRACT_TO_METHOD_TITLE,
|
66
|
+
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
67
|
+
data: { range: @range, uri: @uri.to_s },
|
68
|
+
)
|
69
|
+
end
|
60
70
|
|
61
|
-
|
62
|
-
def refactor_code_action(range, uri)
|
63
|
-
Interface::CodeAction.new(
|
64
|
-
title: "Refactor: Extract Variable",
|
65
|
-
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
66
|
-
data: {
|
67
|
-
range: range,
|
68
|
-
uri: uri.to_s,
|
69
|
-
},
|
70
|
-
)
|
71
|
+
code_actions
|
71
72
|
end
|
72
73
|
end
|
73
74
|
end
|
@@ -11,11 +11,13 @@ module RubyLsp
|
|
11
11
|
# suggests possible completions according to what the developer is typing.
|
12
12
|
#
|
13
13
|
# Currently supported targets:
|
14
|
+
#
|
14
15
|
# - Classes
|
15
16
|
# - Modules
|
16
17
|
# - Constants
|
17
18
|
# - Require paths
|
18
19
|
# - Methods invoked on self only
|
20
|
+
# - Instance variables
|
19
21
|
#
|
20
22
|
# # Example
|
21
23
|
#
|
@@ -34,7 +36,7 @@ module RubyLsp
|
|
34
36
|
def provider
|
35
37
|
Interface::CompletionOptions.new(
|
36
38
|
resolve_provider: true,
|
37
|
-
trigger_characters: ["/", "\"", "'"],
|
39
|
+
trigger_characters: ["/", "\"", "'", ":", "@"],
|
38
40
|
completion_item: {
|
39
41
|
labelDetailsSupport: true,
|
40
42
|
},
|
@@ -58,10 +60,20 @@ module RubyLsp
|
|
58
60
|
# Completion always receives the position immediately after the character that was just typed. Here we adjust it
|
59
61
|
# back by 1, so that we find the right node
|
60
62
|
char_position = document.create_scanner.find_char_position(position) - 1
|
61
|
-
|
63
|
+
node_context = document.locate(
|
62
64
|
document.tree,
|
63
65
|
char_position,
|
64
|
-
node_types: [
|
66
|
+
node_types: [
|
67
|
+
Prism::CallNode,
|
68
|
+
Prism::ConstantReadNode,
|
69
|
+
Prism::ConstantPathNode,
|
70
|
+
Prism::InstanceVariableReadNode,
|
71
|
+
Prism::InstanceVariableAndWriteNode,
|
72
|
+
Prism::InstanceVariableOperatorWriteNode,
|
73
|
+
Prism::InstanceVariableOrWriteNode,
|
74
|
+
Prism::InstanceVariableTargetNode,
|
75
|
+
Prism::InstanceVariableWriteNode,
|
76
|
+
],
|
65
77
|
)
|
66
78
|
@response_builder = T.let(
|
67
79
|
ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem].new,
|
@@ -71,27 +83,24 @@ module RubyLsp
|
|
71
83
|
Listeners::Completion.new(
|
72
84
|
@response_builder,
|
73
85
|
global_state,
|
74
|
-
|
86
|
+
node_context,
|
75
87
|
typechecker_enabled,
|
76
88
|
dispatcher,
|
77
89
|
document.uri,
|
78
90
|
)
|
79
91
|
|
80
92
|
Addon.addons.each do |addon|
|
81
|
-
addon.create_completion_listener(@response_builder,
|
93
|
+
addon.create_completion_listener(@response_builder, node_context, dispatcher, document.uri)
|
82
94
|
end
|
83
95
|
|
96
|
+
matched = node_context.node
|
97
|
+
parent = node_context.parent
|
84
98
|
return unless matched && parent
|
85
99
|
|
86
|
-
@target =
|
87
|
-
|
100
|
+
@target = if parent.is_a?(Prism::ConstantPathNode) && matched.is_a?(Prism::ConstantReadNode)
|
101
|
+
parent
|
102
|
+
else
|
88
103
|
matched
|
89
|
-
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
90
|
-
if parent.is_a?(Prism::ConstantPathNode) && matched.is_a?(Prism::ConstantReadNode)
|
91
|
-
parent
|
92
|
-
else
|
93
|
-
matched
|
94
|
-
end
|
95
104
|
end
|
96
105
|
end
|
97
106
|
|