ruby-lsp 0.27.0.beta2 → 0.27.0.beta4
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/VERSION +1 -1
- data/exe/ruby-lsp +0 -46
- data/exe/ruby-lsp-check +0 -15
- data/lib/ruby_lsp/base_server.rb +2 -0
- data/lib/ruby_lsp/global_state.rb +0 -5
- data/lib/ruby_lsp/internal.rb +2 -1
- data/lib/ruby_lsp/listeners/code_lens.rb +1 -1
- data/lib/ruby_lsp/listeners/completion.rb +244 -383
- data/lib/ruby_lsp/listeners/definition.rb +6 -9
- data/lib/ruby_lsp/listeners/hover.rb +11 -9
- data/lib/ruby_lsp/listeners/signature_help.rb +11 -12
- data/lib/ruby_lsp/listeners/test_discovery.rb +17 -1
- data/lib/ruby_lsp/listeners/test_style.rb +1 -1
- data/lib/ruby_lsp/requests/completion_resolve.rb +49 -29
- data/lib/ruby_lsp/requests/references.rb +21 -7
- data/lib/ruby_lsp/requests/rename.rb +1 -1
- data/lib/ruby_lsp/requests/support/common.rb +79 -68
- data/lib/ruby_lsp/ruby_document.rb +0 -73
- data/lib/ruby_lsp/rubydex/declaration.rb +128 -2
- data/lib/ruby_lsp/rubydex/definition.rb +16 -0
- data/lib/ruby_lsp/rubydex/signature.rb +107 -0
- data/lib/ruby_lsp/scripts/compose_bundle.rb +1 -1
- data/lib/ruby_lsp/server.rb +40 -168
- data/lib/ruby_lsp/setup_bundler.rb +1 -1
- data/lib/ruby_lsp/test_helper.rb +0 -1
- data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +1 -1
- data/lib/ruby_lsp/type_inferrer.rb +58 -40
- metadata +14 -17
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +0 -276
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +0 -1101
- data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +0 -44
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +0 -605
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +0 -1077
- data/lib/ruby_indexer/lib/ruby_indexer/location.rb +0 -37
- data/lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb +0 -149
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +0 -294
- data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +0 -32
- data/lib/ruby_indexer/ruby_indexer.rb +0 -19
- /data/lib/{ruby_indexer/lib/ruby_indexer → ruby_lsp}/uri.rb +0 -0
|
@@ -6,50 +6,6 @@ module RubyLsp
|
|
|
6
6
|
class Completion
|
|
7
7
|
include Requests::Support::Common
|
|
8
8
|
|
|
9
|
-
KEYWORDS = [
|
|
10
|
-
"alias",
|
|
11
|
-
"and",
|
|
12
|
-
"begin",
|
|
13
|
-
"BEGIN",
|
|
14
|
-
"break",
|
|
15
|
-
"case",
|
|
16
|
-
"class",
|
|
17
|
-
"def",
|
|
18
|
-
"defined?",
|
|
19
|
-
"do",
|
|
20
|
-
"else",
|
|
21
|
-
"elsif",
|
|
22
|
-
"end",
|
|
23
|
-
"END",
|
|
24
|
-
"ensure",
|
|
25
|
-
"false",
|
|
26
|
-
"for",
|
|
27
|
-
"if",
|
|
28
|
-
"in",
|
|
29
|
-
"module",
|
|
30
|
-
"next",
|
|
31
|
-
"nil",
|
|
32
|
-
"not",
|
|
33
|
-
"or",
|
|
34
|
-
"redo",
|
|
35
|
-
"rescue",
|
|
36
|
-
"retry",
|
|
37
|
-
"return",
|
|
38
|
-
"self",
|
|
39
|
-
"super",
|
|
40
|
-
"then",
|
|
41
|
-
"true",
|
|
42
|
-
"undef",
|
|
43
|
-
"unless",
|
|
44
|
-
"until",
|
|
45
|
-
"when",
|
|
46
|
-
"while",
|
|
47
|
-
"yield",
|
|
48
|
-
"__ENCODING__",
|
|
49
|
-
"__FILE__",
|
|
50
|
-
"__LINE__",
|
|
51
|
-
].freeze
|
|
52
|
-
|
|
53
9
|
#: (ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem] response_builder, GlobalState global_state, NodeContext node_context, SorbetLevel sorbet_level, Prism::Dispatcher dispatcher, URI::Generic uri, String? trigger_character) -> void
|
|
54
10
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
55
11
|
response_builder,
|
|
@@ -62,7 +18,7 @@ module RubyLsp
|
|
|
62
18
|
)
|
|
63
19
|
@response_builder = response_builder
|
|
64
20
|
@global_state = global_state
|
|
65
|
-
@
|
|
21
|
+
@graph = global_state.graph #: Rubydex::Graph
|
|
66
22
|
@type_inferrer = global_state.type_inferrer #: TypeInferrer
|
|
67
23
|
@node_context = node_context
|
|
68
24
|
@sorbet_level = sorbet_level
|
|
@@ -102,22 +58,10 @@ module RubyLsp
|
|
|
102
58
|
# no sigil, Sorbet will still provide completion for constants
|
|
103
59
|
return unless @sorbet_level.ignore?
|
|
104
60
|
|
|
105
|
-
name =
|
|
61
|
+
name = constant_name(node)
|
|
106
62
|
return if name.nil?
|
|
107
63
|
|
|
108
|
-
|
|
109
|
-
candidates = @index.constant_completion_candidates(name, @node_context.nesting)
|
|
110
|
-
candidates.each do |entries|
|
|
111
|
-
complete_name = entries.first #: as !nil
|
|
112
|
-
.name
|
|
113
|
-
@response_builder << build_entry_completion(
|
|
114
|
-
complete_name,
|
|
115
|
-
name,
|
|
116
|
-
range,
|
|
117
|
-
entries,
|
|
118
|
-
top_level?(complete_name),
|
|
119
|
-
)
|
|
120
|
-
end
|
|
64
|
+
complete_constants(range_from_location(node.location), name)
|
|
121
65
|
end
|
|
122
66
|
|
|
123
67
|
# Handle completion on namespaced constant references (e.g. `Foo::Bar`)
|
|
@@ -152,7 +96,7 @@ module RubyLsp
|
|
|
152
96
|
if (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)) &&
|
|
153
97
|
node.call_operator == "::"
|
|
154
98
|
|
|
155
|
-
name =
|
|
99
|
+
name = constant_name(receiver)
|
|
156
100
|
|
|
157
101
|
if name
|
|
158
102
|
start_loc = node.location
|
|
@@ -179,245 +123,295 @@ module RubyLsp
|
|
|
179
123
|
when "require_relative"
|
|
180
124
|
complete_require_relative(node)
|
|
181
125
|
else
|
|
182
|
-
|
|
126
|
+
if node.receiver
|
|
127
|
+
complete_method_call(node, name)
|
|
128
|
+
else
|
|
129
|
+
# Sorbet provides method-on-self completion for any file with a Sorbet level of true or higher
|
|
130
|
+
return if @sorbet_level.true_or_higher?
|
|
131
|
+
|
|
132
|
+
message_loc = node.message_loc #: as !nil
|
|
133
|
+
complete_methods_no_receiver(range_from_location(message_loc), name)
|
|
134
|
+
end
|
|
183
135
|
end
|
|
184
136
|
end
|
|
185
137
|
|
|
186
138
|
#: (Prism::GlobalVariableAndWriteNode node) -> void
|
|
187
139
|
def on_global_variable_and_write_node_enter(node)
|
|
188
|
-
|
|
140
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
189
141
|
end
|
|
190
142
|
|
|
191
143
|
#: (Prism::GlobalVariableOperatorWriteNode node) -> void
|
|
192
144
|
def on_global_variable_operator_write_node_enter(node)
|
|
193
|
-
|
|
145
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
194
146
|
end
|
|
195
147
|
|
|
196
148
|
#: (Prism::GlobalVariableOrWriteNode node) -> void
|
|
197
149
|
def on_global_variable_or_write_node_enter(node)
|
|
198
|
-
|
|
150
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
199
151
|
end
|
|
200
152
|
|
|
201
153
|
#: (Prism::GlobalVariableReadNode node) -> void
|
|
202
154
|
def on_global_variable_read_node_enter(node)
|
|
203
|
-
|
|
155
|
+
complete_variable(range_from_location(node.location), node.name.to_s)
|
|
204
156
|
end
|
|
205
157
|
|
|
206
158
|
#: (Prism::GlobalVariableTargetNode node) -> void
|
|
207
159
|
def on_global_variable_target_node_enter(node)
|
|
208
|
-
|
|
160
|
+
complete_variable(range_from_location(node.location), node.name.to_s)
|
|
209
161
|
end
|
|
210
162
|
|
|
211
163
|
#: (Prism::GlobalVariableWriteNode node) -> void
|
|
212
164
|
def on_global_variable_write_node_enter(node)
|
|
213
|
-
|
|
165
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
214
166
|
end
|
|
215
167
|
|
|
216
168
|
#: (Prism::InstanceVariableReadNode node) -> void
|
|
217
169
|
def on_instance_variable_read_node_enter(node)
|
|
218
|
-
|
|
170
|
+
return if @sorbet_level.strict?
|
|
171
|
+
|
|
172
|
+
complete_variable(range_from_location(node.location), node.name.to_s)
|
|
219
173
|
end
|
|
220
174
|
|
|
221
175
|
#: (Prism::InstanceVariableWriteNode node) -> void
|
|
222
176
|
def on_instance_variable_write_node_enter(node)
|
|
223
|
-
|
|
177
|
+
return if @sorbet_level.strict?
|
|
178
|
+
|
|
179
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
224
180
|
end
|
|
225
181
|
|
|
226
182
|
#: (Prism::InstanceVariableAndWriteNode node) -> void
|
|
227
183
|
def on_instance_variable_and_write_node_enter(node)
|
|
228
|
-
|
|
184
|
+
return if @sorbet_level.strict?
|
|
185
|
+
|
|
186
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
229
187
|
end
|
|
230
188
|
|
|
231
189
|
#: (Prism::InstanceVariableOperatorWriteNode node) -> void
|
|
232
190
|
def on_instance_variable_operator_write_node_enter(node)
|
|
233
|
-
|
|
191
|
+
return if @sorbet_level.strict?
|
|
192
|
+
|
|
193
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
234
194
|
end
|
|
235
195
|
|
|
236
196
|
#: (Prism::InstanceVariableOrWriteNode node) -> void
|
|
237
197
|
def on_instance_variable_or_write_node_enter(node)
|
|
238
|
-
|
|
198
|
+
return if @sorbet_level.strict?
|
|
199
|
+
|
|
200
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
239
201
|
end
|
|
240
202
|
|
|
241
203
|
#: (Prism::InstanceVariableTargetNode node) -> void
|
|
242
204
|
def on_instance_variable_target_node_enter(node)
|
|
243
|
-
|
|
205
|
+
return if @sorbet_level.strict?
|
|
206
|
+
|
|
207
|
+
complete_variable(range_from_location(node.location), node.name.to_s)
|
|
244
208
|
end
|
|
245
209
|
|
|
246
210
|
#: (Prism::ClassVariableAndWriteNode node) -> void
|
|
247
211
|
def on_class_variable_and_write_node_enter(node)
|
|
248
|
-
|
|
212
|
+
return if @sorbet_level.strict?
|
|
213
|
+
|
|
214
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
249
215
|
end
|
|
250
216
|
|
|
251
217
|
#: (Prism::ClassVariableOperatorWriteNode node) -> void
|
|
252
218
|
def on_class_variable_operator_write_node_enter(node)
|
|
253
|
-
|
|
219
|
+
return if @sorbet_level.strict?
|
|
220
|
+
|
|
221
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
254
222
|
end
|
|
255
223
|
|
|
256
224
|
#: (Prism::ClassVariableOrWriteNode node) -> void
|
|
257
225
|
def on_class_variable_or_write_node_enter(node)
|
|
258
|
-
|
|
226
|
+
return if @sorbet_level.strict?
|
|
227
|
+
|
|
228
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
259
229
|
end
|
|
260
230
|
|
|
261
231
|
#: (Prism::ClassVariableTargetNode node) -> void
|
|
262
232
|
def on_class_variable_target_node_enter(node)
|
|
263
|
-
|
|
233
|
+
return if @sorbet_level.strict?
|
|
234
|
+
|
|
235
|
+
complete_variable(range_from_location(node.location), node.name.to_s)
|
|
264
236
|
end
|
|
265
237
|
|
|
266
238
|
#: (Prism::ClassVariableReadNode node) -> void
|
|
267
239
|
def on_class_variable_read_node_enter(node)
|
|
268
|
-
|
|
240
|
+
return if @sorbet_level.strict?
|
|
241
|
+
|
|
242
|
+
complete_variable(range_from_location(node.location), node.name.to_s)
|
|
269
243
|
end
|
|
270
244
|
|
|
271
245
|
#: (Prism::ClassVariableWriteNode node) -> void
|
|
272
246
|
def on_class_variable_write_node_enter(node)
|
|
273
|
-
|
|
247
|
+
return if @sorbet_level.strict?
|
|
248
|
+
|
|
249
|
+
complete_variable(range_from_location(node.name_loc), node.name.to_s)
|
|
274
250
|
end
|
|
275
251
|
|
|
276
252
|
private
|
|
277
253
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
end
|
|
254
|
+
# Returns every candidate reachable from the current scope (constants, methods, ivars, cvars, globals, keywords).
|
|
255
|
+
# Specialized completion methods filter by node kind and prefix.
|
|
256
|
+
#
|
|
257
|
+
#: () -> Array[(Rubydex::Declaration | Rubydex::Keyword)]
|
|
258
|
+
def expression_candidates
|
|
259
|
+
@graph.complete_expression(@node_context.nesting, self_receiver: @type_inferrer.self_receiver_name(@node_context))
|
|
260
|
+
end
|
|
286
261
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
*namespace, incomplete_name = name.split("::")
|
|
293
|
-
namespace.join("::")
|
|
294
|
-
end
|
|
262
|
+
#: (Interface::Range range, String prefix) -> void
|
|
263
|
+
def complete_constants(range, prefix)
|
|
264
|
+
expression_candidates.each do |candidate|
|
|
265
|
+
next unless candidate.is_a?(Rubydex::Class) || candidate.is_a?(Rubydex::Module) ||
|
|
266
|
+
candidate.is_a?(Rubydex::Constant) || candidate.is_a?(Rubydex::ConstantAlias)
|
|
295
267
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
268
|
+
# Match either the short (unqualified) or fully qualified name, so that lexically-reachable constants like
|
|
269
|
+
# `Foo::CONST` match when the user types `CONST` and fully-qualified typing like `Foo::CONST` still matches
|
|
270
|
+
complete_name = candidate.name
|
|
271
|
+
next unless candidate.unqualified_name.start_with?(prefix) || complete_name.start_with?(prefix)
|
|
299
272
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
273
|
+
@response_builder << build_entry_completion(complete_name, prefix, range, candidate)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
303
276
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
# The only time we may have a private constant reference from outside of the namespace is if we're dealing
|
|
310
|
-
# with ConstantPath and the entry name doesn't start with the current nesting
|
|
311
|
-
first_entry = entries.first #: as !nil
|
|
312
|
-
next if first_entry.private? && !first_entry.name.start_with?("#{nesting}::")
|
|
313
|
-
|
|
314
|
-
entry_name = first_entry.name
|
|
315
|
-
full_name = if aliased_namespace != real_namespace
|
|
316
|
-
constant_name = entry_name.delete_prefix("#{real_namespace}::")
|
|
317
|
-
aliased_namespace.empty? ? constant_name : "#{aliased_namespace}::#{constant_name}"
|
|
318
|
-
elsif !entry_name.start_with?(aliased_namespace)
|
|
319
|
-
*_, short_name = entry_name.split("::")
|
|
320
|
-
"#{aliased_namespace}::#{short_name}"
|
|
321
|
-
else
|
|
322
|
-
entry_name
|
|
323
|
-
end
|
|
277
|
+
#: (Interface::Range range, String prefix) -> void
|
|
278
|
+
def complete_methods_no_receiver(range, prefix)
|
|
279
|
+
@node_context.locals_for_scope.each do |local|
|
|
280
|
+
local_name = local.to_s
|
|
281
|
+
next unless local_name.start_with?(prefix)
|
|
324
282
|
|
|
325
|
-
@response_builder <<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
range,
|
|
329
|
-
|
|
330
|
-
|
|
283
|
+
@response_builder << Interface::CompletionItem.new(
|
|
284
|
+
label: local_name,
|
|
285
|
+
filter_text: local_name,
|
|
286
|
+
text_edit: Interface::TextEdit.new(range: range, new_text: local_name),
|
|
287
|
+
kind: Constant::CompletionItemKind::VARIABLE,
|
|
288
|
+
data: { skip_resolve: true },
|
|
331
289
|
)
|
|
332
290
|
end
|
|
291
|
+
|
|
292
|
+
expression_candidates.each do |candidate|
|
|
293
|
+
case candidate
|
|
294
|
+
when Rubydex::Method
|
|
295
|
+
display_name = candidate.unqualified_name.delete_suffix("()")
|
|
296
|
+
next unless display_name.start_with?(prefix)
|
|
297
|
+
|
|
298
|
+
add_method_completion(candidate, range)
|
|
299
|
+
when Rubydex::Keyword
|
|
300
|
+
next unless candidate.name.start_with?(prefix)
|
|
301
|
+
|
|
302
|
+
@response_builder << Interface::CompletionItem.new(
|
|
303
|
+
label: candidate.name,
|
|
304
|
+
text_edit: Interface::TextEdit.new(range: range, new_text: candidate.name),
|
|
305
|
+
kind: Constant::CompletionItemKind::KEYWORD,
|
|
306
|
+
data: { keyword: true },
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
333
310
|
end
|
|
334
311
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
312
|
+
# Namespace access (e.g.: `Foo::Bar`, `::Bar`). Collects all constants for the namespace that the prefix resolves
|
|
313
|
+
# to, preserving any alias names typed by the user
|
|
314
|
+
#: (String name, Interface::Range range) -> void
|
|
315
|
+
def constant_path_completion(name, range)
|
|
316
|
+
if name.end_with?("::")
|
|
317
|
+
namespace_prefix = name.delete_suffix("::")
|
|
318
|
+
incomplete_name = nil
|
|
319
|
+
else
|
|
320
|
+
*segments, incomplete_name = name.split("::")
|
|
321
|
+
namespace_prefix = segments.join("::")
|
|
322
|
+
end
|
|
338
323
|
|
|
339
|
-
|
|
324
|
+
candidates = if namespace_prefix.empty?
|
|
325
|
+
@graph.complete_expression([], self_receiver: @type_inferrer.self_receiver_name(@node_context))
|
|
326
|
+
else
|
|
327
|
+
# Rubydex's resolver handles a leading `::` on `namespace_prefix` by resolving from the top-level scope, so
|
|
328
|
+
# we don't need to special-case top-level references here
|
|
329
|
+
resolved = @graph.resolve_constant(namespace_prefix, @node_context.nesting)
|
|
330
|
+
return unless resolved
|
|
340
331
|
|
|
341
|
-
|
|
332
|
+
@graph.complete_namespace_access(resolved.name, self_receiver: @type_inferrer.self_receiver_name(@node_context))
|
|
333
|
+
end
|
|
342
334
|
|
|
343
|
-
candidates.
|
|
344
|
-
|
|
335
|
+
candidates.each do |candidate|
|
|
336
|
+
next unless candidate.is_a?(Rubydex::Class) || candidate.is_a?(Rubydex::Module) ||
|
|
337
|
+
candidate.is_a?(Rubydex::Constant) || candidate.is_a?(Rubydex::ConstantAlias)
|
|
345
338
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
),
|
|
352
|
-
text_edit: Interface::TextEdit.new(range: range, new_text: entry_name),
|
|
353
|
-
kind: Constant::CompletionItemKind::VARIABLE,
|
|
354
|
-
)
|
|
339
|
+
short_name = candidate.unqualified_name
|
|
340
|
+
next if incomplete_name && !short_name.start_with?(incomplete_name)
|
|
341
|
+
|
|
342
|
+
full_name = namespace_prefix.empty? ? short_name : "#{namespace_prefix}::#{short_name}"
|
|
343
|
+
@response_builder << build_entry_completion(full_name, name, range, candidate)
|
|
355
344
|
end
|
|
356
345
|
end
|
|
357
346
|
|
|
358
|
-
|
|
359
|
-
|
|
347
|
+
# Method call on a receiver (e.g.: `foo.`, `@bar.`, `@@baz.`, `Qux.`). Collects all methods that exist on the
|
|
348
|
+
# type returned by the receiver, filtered by the prefix typed
|
|
349
|
+
#: (Prism::CallNode node, String name) -> void
|
|
350
|
+
def complete_method_call(node, name)
|
|
351
|
+
# Sorbet can provide completion for methods invoked on self on typed true or higher files
|
|
352
|
+
return if @sorbet_level.true_or_higher? && self_receiver?(node)
|
|
353
|
+
|
|
360
354
|
type = @type_inferrer.infer_receiver_type(@node_context)
|
|
361
355
|
return unless type
|
|
362
356
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
357
|
+
# When the trigger character is a dot, Prism matches the name of the call node to whatever is next in the
|
|
358
|
+
# source code, leading to us searching for the wrong name. What we want to do instead is show every available
|
|
359
|
+
# method when dot is pressed
|
|
360
|
+
method_name = @trigger_character == "." ? nil : name
|
|
367
361
|
|
|
368
|
-
|
|
369
|
-
|
|
362
|
+
range = if method_name
|
|
363
|
+
range_from_location(
|
|
364
|
+
node.message_loc, #: as !nil
|
|
370
365
|
)
|
|
366
|
+
else
|
|
367
|
+
loc = node.call_operator_loc
|
|
371
368
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
),
|
|
379
|
-
kind: Constant::CompletionItemKind::FIELD,
|
|
380
|
-
data: {
|
|
381
|
-
owner_name: entry.owner&.name,
|
|
382
|
-
},
|
|
383
|
-
)
|
|
369
|
+
if loc
|
|
370
|
+
Interface::Range.new(
|
|
371
|
+
start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column + 1),
|
|
372
|
+
end: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column + 1),
|
|
373
|
+
)
|
|
374
|
+
end
|
|
384
375
|
end
|
|
385
|
-
rescue RubyIndexer::Index::NonExistingNamespaceError
|
|
386
|
-
# If by any chance we haven't indexed the owner, then there's no way to find the right declaration
|
|
387
|
-
end
|
|
388
376
|
|
|
389
|
-
|
|
390
|
-
def handle_instance_variable_completion(name, location)
|
|
391
|
-
# Sorbet enforces that all instance variables be declared on typed strict or higher, which means it will be able
|
|
392
|
-
# to provide all features for them
|
|
393
|
-
return if @sorbet_level.strict?
|
|
377
|
+
return unless range
|
|
394
378
|
|
|
395
|
-
|
|
396
|
-
return unless type
|
|
379
|
+
guessed_type = type.is_a?(TypeInferrer::GuessedType) && type.name
|
|
397
380
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
381
|
+
@graph.complete_method_call(type.name, self_receiver: @type_inferrer.self_receiver_name(@node_context)).each do |candidate|
|
|
382
|
+
if method_name
|
|
383
|
+
display_name = candidate.unqualified_name.delete_suffix("()")
|
|
384
|
+
next unless display_name.start_with?(method_name)
|
|
385
|
+
end
|
|
401
386
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
387
|
+
add_method_completion(candidate, range, has_receiver: true, guessed_type: guessed_type)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Variable completion (instance, class, and global). The variable kind is selected by the prefix the user typed:
|
|
392
|
+
# `$…` only matches globals, `@@…` only class variables, and `@…` matches both instance and class variables (since
|
|
393
|
+
# `@@foo`.start_with?("@") is true). This uses the same query as expression completion: the lexical nesting drives
|
|
394
|
+
# constants and class variables, while the `self` type (which may differ from the nesting, e.g. inside
|
|
395
|
+
# `def self.foo` or `def Bar.baz`) drives instance variables.
|
|
396
|
+
#
|
|
397
|
+
#: (Interface::Range, String) -> void
|
|
398
|
+
def complete_variable(range, prefix)
|
|
399
|
+
expression_candidates.each do |candidate|
|
|
400
|
+
next unless candidate.is_a?(Rubydex::Declaration)
|
|
401
|
+
|
|
402
|
+
variable_name = candidate.unqualified_name
|
|
403
|
+
next unless variable_name.start_with?(prefix)
|
|
405
404
|
|
|
406
405
|
@response_builder << Interface::CompletionItem.new(
|
|
407
406
|
label: variable_name,
|
|
408
|
-
label_details:
|
|
409
|
-
|
|
410
|
-
range: range,
|
|
411
|
-
new_text: variable_name,
|
|
407
|
+
label_details: Interface::CompletionItemLabelDetails.new(
|
|
408
|
+
description: declaration_file_names(candidate),
|
|
412
409
|
),
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
},
|
|
410
|
+
text_edit: Interface::TextEdit.new(range: range, new_text: variable_name),
|
|
411
|
+
kind: candidate.to_lsp_completion_kind,
|
|
412
|
+
data: { owner_name: candidate.owner.name },
|
|
417
413
|
)
|
|
418
414
|
end
|
|
419
|
-
rescue RubyIndexer::Index::NonExistingNamespaceError
|
|
420
|
-
# If by any chance we haven't indexed the owner, then there's no way to find the right declaration
|
|
421
415
|
end
|
|
422
416
|
|
|
423
417
|
#: (Prism::CallNode node) -> void
|
|
@@ -429,13 +423,10 @@ module RubyLsp
|
|
|
429
423
|
|
|
430
424
|
return unless path_node_to_complete.is_a?(Prism::StringNode)
|
|
431
425
|
|
|
432
|
-
|
|
426
|
+
content = path_node_to_complete.content
|
|
433
427
|
|
|
434
|
-
|
|
435
|
-
@response_builder << build_completion(
|
|
436
|
-
path, #: as !nil
|
|
437
|
-
path_node_to_complete,
|
|
438
|
-
)
|
|
428
|
+
@graph.require_paths($LOAD_PATH).select { |path| path.start_with?(content) }.sort!.each do |path|
|
|
429
|
+
@response_builder << build_completion(path, path_node_to_complete)
|
|
439
430
|
end
|
|
440
431
|
end
|
|
441
432
|
|
|
@@ -474,120 +465,26 @@ module RubyLsp
|
|
|
474
465
|
# might fail with EPERM
|
|
475
466
|
end
|
|
476
467
|
|
|
477
|
-
#: (
|
|
478
|
-
def
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if !@sorbet_level.true_or_higher? && !node.receiver
|
|
482
|
-
add_local_completions(node, name)
|
|
483
|
-
add_keyword_completions(node, name)
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
# Sorbet can provide completion for methods invoked on self on typed true or higher files
|
|
487
|
-
return if @sorbet_level.true_or_higher? && self_receiver?(node)
|
|
488
|
-
|
|
489
|
-
type = @type_inferrer.infer_receiver_type(@node_context)
|
|
490
|
-
return unless type
|
|
491
|
-
|
|
492
|
-
# When the trigger character is a dot, Prism matches the name of the call node to whatever is next in the source
|
|
493
|
-
# code, leading to us searching for the wrong name. What we want to do instead is show every available method
|
|
494
|
-
# when dot is pressed
|
|
495
|
-
method_name = @trigger_character == "." ? nil : name
|
|
496
|
-
|
|
497
|
-
range = if method_name
|
|
498
|
-
range_from_location(
|
|
499
|
-
node.message_loc, #: as !nil
|
|
500
|
-
)
|
|
501
|
-
else
|
|
502
|
-
loc = node.call_operator_loc
|
|
503
|
-
|
|
504
|
-
if loc
|
|
505
|
-
Interface::Range.new(
|
|
506
|
-
start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column + 1),
|
|
507
|
-
end: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column + 1),
|
|
508
|
-
)
|
|
509
|
-
end
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
return unless range
|
|
513
|
-
|
|
514
|
-
guessed_type = type.is_a?(TypeInferrer::GuessedType) && type.name
|
|
515
|
-
external_references = @node_context.fully_qualified_name != type.name
|
|
516
|
-
|
|
517
|
-
@index.method_completion_candidates(method_name, type.name).each do |entry|
|
|
518
|
-
next if entry.visibility != :public && external_references
|
|
519
|
-
|
|
520
|
-
entry_name = entry.name
|
|
521
|
-
owner_name = entry.owner&.name
|
|
522
|
-
new_text = entry_name
|
|
523
|
-
|
|
524
|
-
if entry_name.end_with?("=")
|
|
525
|
-
method_name = entry_name.delete_suffix("=")
|
|
526
|
-
|
|
527
|
-
# For writer methods, format as assignment and prefix "self." when no receiver is specified
|
|
528
|
-
new_text = node.receiver.nil? ? "self.#{method_name} = " : "#{method_name} = "
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
label_details = Interface::CompletionItemLabelDetails.new(
|
|
532
|
-
description: entry.file_name,
|
|
533
|
-
detail: entry.decorated_parameters,
|
|
534
|
-
)
|
|
535
|
-
@response_builder << Interface::CompletionItem.new(
|
|
536
|
-
label: entry_name,
|
|
537
|
-
filter_text: entry_name,
|
|
538
|
-
label_details: label_details,
|
|
539
|
-
text_edit: Interface::TextEdit.new(range: range, new_text: new_text),
|
|
540
|
-
kind: Constant::CompletionItemKind::METHOD,
|
|
541
|
-
data: {
|
|
542
|
-
owner_name: owner_name,
|
|
543
|
-
guessed_type: guessed_type,
|
|
544
|
-
},
|
|
545
|
-
)
|
|
546
|
-
end
|
|
547
|
-
rescue RubyIndexer::Index::NonExistingNamespaceError
|
|
548
|
-
# We have not indexed this namespace, so we can't provide any completions
|
|
549
|
-
end
|
|
550
|
-
|
|
551
|
-
#: (Prism::CallNode node, String name) -> void
|
|
552
|
-
def add_local_completions(node, name)
|
|
553
|
-
range = range_from_location(
|
|
554
|
-
node.message_loc, #: as !nil
|
|
555
|
-
)
|
|
468
|
+
#: (Rubydex::Method candidate, Interface::Range range, ?has_receiver: bool, ?guessed_type: (String | bool)) -> void
|
|
469
|
+
def add_method_completion(candidate, range, has_receiver: false, guessed_type: false)
|
|
470
|
+
display_name = candidate.unqualified_name.delete_suffix("()")
|
|
471
|
+
new_text = display_name
|
|
556
472
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
next unless local_name.start_with?(name)
|
|
473
|
+
if display_name.end_with?("=")
|
|
474
|
+
setter_name = display_name.delete_suffix("=")
|
|
560
475
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
filter_text: local_name,
|
|
564
|
-
text_edit: Interface::TextEdit.new(range: range, new_text: local_name),
|
|
565
|
-
kind: Constant::CompletionItemKind::VARIABLE,
|
|
566
|
-
data: {
|
|
567
|
-
skip_resolve: true,
|
|
568
|
-
},
|
|
569
|
-
)
|
|
476
|
+
# For writer methods, format as assignment and prefix "self." when no receiver is specified
|
|
477
|
+
new_text = has_receiver ? "#{setter_name} = " : "self.#{setter_name} = "
|
|
570
478
|
end
|
|
571
|
-
end
|
|
572
479
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
480
|
+
@response_builder << Interface::CompletionItem.new(
|
|
481
|
+
label: display_name,
|
|
482
|
+
filter_text: display_name,
|
|
483
|
+
label_details: Interface::CompletionItemLabelDetails.new(description: declaration_file_names(candidate)),
|
|
484
|
+
text_edit: Interface::TextEdit.new(range: range, new_text: new_text),
|
|
485
|
+
kind: candidate.to_lsp_completion_kind,
|
|
486
|
+
data: { owner_name: candidate.owner.name, guessed_type: guessed_type },
|
|
577
487
|
)
|
|
578
|
-
|
|
579
|
-
KEYWORDS.each do |keyword|
|
|
580
|
-
next unless keyword.start_with?(name)
|
|
581
|
-
|
|
582
|
-
@response_builder << Interface::CompletionItem.new(
|
|
583
|
-
label: keyword,
|
|
584
|
-
text_edit: Interface::TextEdit.new(range: range, new_text: keyword),
|
|
585
|
-
kind: Constant::CompletionItemKind::KEYWORD,
|
|
586
|
-
data: {
|
|
587
|
-
keyword: true,
|
|
588
|
-
},
|
|
589
|
-
)
|
|
590
|
-
end
|
|
591
488
|
end
|
|
592
489
|
|
|
593
490
|
#: (String label, Prism::StringNode node) -> Interface::CompletionItem
|
|
@@ -605,68 +502,30 @@ module RubyLsp
|
|
|
605
502
|
)
|
|
606
503
|
end
|
|
607
504
|
|
|
608
|
-
#: (String real_name, String incomplete_name, Interface::Range range,
|
|
609
|
-
def build_entry_completion(real_name, incomplete_name, range,
|
|
610
|
-
first_entry = entries.first #: as !nil
|
|
611
|
-
kind = case first_entry
|
|
612
|
-
when RubyIndexer::Entry::Class
|
|
613
|
-
Constant::CompletionItemKind::CLASS
|
|
614
|
-
when RubyIndexer::Entry::Module
|
|
615
|
-
Constant::CompletionItemKind::MODULE
|
|
616
|
-
when RubyIndexer::Entry::Constant
|
|
617
|
-
Constant::CompletionItemKind::CONSTANT
|
|
618
|
-
else
|
|
619
|
-
Constant::CompletionItemKind::REFERENCE
|
|
620
|
-
end
|
|
621
|
-
|
|
505
|
+
#: (String real_name, String incomplete_name, Interface::Range range, Rubydex::Declaration declaration) -> Interface::CompletionItem
|
|
506
|
+
def build_entry_completion(real_name, incomplete_name, range, declaration)
|
|
622
507
|
insertion_text = real_name.dup
|
|
623
508
|
filter_text = real_name.dup
|
|
624
509
|
|
|
625
|
-
#
|
|
626
|
-
#
|
|
627
|
-
#
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
# end
|
|
641
|
-
#
|
|
642
|
-
# Foo::B # --> completion inserts `Bar` instead of `Foo::Bar`
|
|
643
|
-
# end
|
|
644
|
-
nesting = @node_context.nesting
|
|
645
|
-
unless @node_context.fully_qualified_name.start_with?(incomplete_name)
|
|
646
|
-
nesting.each do |namespace|
|
|
647
|
-
prefix = "#{namespace}::"
|
|
648
|
-
shortened_name = insertion_text.delete_prefix(prefix)
|
|
649
|
-
|
|
650
|
-
# If a different entry exists for the shortened name, then there's a conflict and we should not shorten it
|
|
651
|
-
conflict_name = "#{@node_context.fully_qualified_name}::#{shortened_name}"
|
|
652
|
-
break if real_name != conflict_name && @index[conflict_name]
|
|
653
|
-
|
|
654
|
-
insertion_text = shortened_name
|
|
655
|
-
|
|
656
|
-
# If the user is typing a fully qualified name `Foo::Bar::Baz`, then we should not use the short name (e.g.:
|
|
657
|
-
# `Baz`) as filtering. So we only shorten the filter text if the user is not including the namespaces in
|
|
658
|
-
# their typing
|
|
659
|
-
filter_text.delete_prefix!(prefix) unless incomplete_name.start_with?(prefix)
|
|
510
|
+
# When the user explicitly typed `::Foo`, the absolute prefix must be preserved and the suffix-shortening
|
|
511
|
+
# below is skipped — replacing `::Bar` with `Bar` would change which constant resolves. The leading `::` is
|
|
512
|
+
# only present on `incomplete_name` (the user-typed text)
|
|
513
|
+
if incomplete_name.start_with?("::")
|
|
514
|
+
insertion_text.prepend("::") unless insertion_text.start_with?("::")
|
|
515
|
+
filter_text.prepend("::") unless filter_text.start_with?("::")
|
|
516
|
+
else
|
|
517
|
+
shortest = shortest_constant_suffix(real_name)
|
|
518
|
+
|
|
519
|
+
if shortest.length < insertion_text.length
|
|
520
|
+
stripped_prefix = real_name.delete_suffix(shortest)
|
|
521
|
+
insertion_text = shortest
|
|
522
|
+
# When the user is typing a more qualified path (e.g. `Foo::B`), keep the filter text qualified so the
|
|
523
|
+
# editor's filter still matches what they typed; otherwise the unqualified suffix is enough
|
|
524
|
+
filter_text = shortest unless incomplete_name.start_with?(stripped_prefix)
|
|
660
525
|
end
|
|
661
526
|
end
|
|
662
527
|
|
|
663
|
-
|
|
664
|
-
# For these top level references, we need to include the `::` as part of the filter text or else it won't match
|
|
665
|
-
# the right entries in the index
|
|
666
|
-
|
|
667
|
-
label_details = Interface::CompletionItemLabelDetails.new(
|
|
668
|
-
description: entries.map(&:file_name).join(","),
|
|
669
|
-
)
|
|
528
|
+
label_details = Interface::CompletionItemLabelDetails.new(description: declaration_file_names(declaration))
|
|
670
529
|
|
|
671
530
|
Interface::CompletionItem.new(
|
|
672
531
|
label: real_name,
|
|
@@ -676,37 +535,39 @@ module RubyLsp
|
|
|
676
535
|
range: range,
|
|
677
536
|
new_text: insertion_text,
|
|
678
537
|
),
|
|
679
|
-
kind:
|
|
538
|
+
kind: declaration.to_lsp_completion_kind,
|
|
539
|
+
data: { fully_qualified_name: declaration.name },
|
|
680
540
|
)
|
|
681
541
|
end
|
|
682
542
|
|
|
683
|
-
#
|
|
684
|
-
# For example:
|
|
543
|
+
# Returns the shortest possible name for a constant reference that still resolves to the same target
|
|
685
544
|
#
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
# module Foo
|
|
690
|
-
# class Bar; end
|
|
691
|
-
#
|
|
692
|
-
# # in this case, the completion for `Bar` conflicts with `Foo::Bar`, so we can't suggest `Bar` as the
|
|
693
|
-
# # completion, but instead need to suggest `::Bar`
|
|
694
|
-
# B
|
|
695
|
-
# end
|
|
696
|
-
# ```
|
|
697
|
-
#: (String entry_name) -> bool
|
|
698
|
-
def top_level?(entry_name)
|
|
545
|
+
#: (String) -> String
|
|
546
|
+
def shortest_constant_suffix(real_name)
|
|
547
|
+
segments = real_name.split("::")
|
|
699
548
|
nesting = @node_context.nesting
|
|
700
|
-
nesting.length.downto(0) do |i|
|
|
701
|
-
prefix = nesting[0...i] #: as !nil
|
|
702
|
-
.join("::")
|
|
703
|
-
full_name = prefix.empty? ? entry_name : "#{prefix}::#{entry_name}"
|
|
704
|
-
next if full_name == entry_name
|
|
705
549
|
|
|
706
|
-
|
|
550
|
+
(1..segments.length).each do |suffix_len|
|
|
551
|
+
suffix = segments.last(suffix_len).join("::")
|
|
552
|
+
resolved = @graph.resolve_constant(suffix, nesting)
|
|
553
|
+
return suffix if resolved && resolved.name == real_name
|
|
707
554
|
end
|
|
708
555
|
|
|
709
|
-
|
|
556
|
+
real_name
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
#: (Rubydex::Declaration declaration) -> String
|
|
560
|
+
def declaration_file_names(declaration)
|
|
561
|
+
declaration.definitions.filter_map do |defn|
|
|
562
|
+
uri = URI(defn.location.uri)
|
|
563
|
+
case uri.scheme
|
|
564
|
+
when "untitled"
|
|
565
|
+
uri.opaque
|
|
566
|
+
when "file"
|
|
567
|
+
path = uri.full_path
|
|
568
|
+
File.basename(path) if path
|
|
569
|
+
end
|
|
570
|
+
end.uniq.join(",")
|
|
710
571
|
end
|
|
711
572
|
end
|
|
712
573
|
end
|