ruby-lsp 0.27.0.beta2 → 0.27.0.beta3
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/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 +246 -382
- 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 +69 -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 +7 -162
- 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 +2 -2
- metadata +11 -14
- 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,298 @@ 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: nil)
|
|
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: nil)
|
|
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: nil)
|
|
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
|
|
|
379
|
+
guessed_type = type.is_a?(TypeInferrer::GuessedType) && type.name
|
|
380
|
+
|
|
381
|
+
@graph.complete_method_call(type.name, self_receiver: nil).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
|
|
386
|
+
|
|
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). Globals live at top-level, so they need an empty nesting; instance/class
|
|
394
|
+
# variables resolve through the type_inferrer to handle singleton methods and class bodies, where the receiver is
|
|
395
|
+
# the singleton class rather than the lexical nesting.
|
|
396
|
+
#
|
|
397
|
+
#: (Interface::Range, String) -> void
|
|
398
|
+
def complete_variable(range, prefix)
|
|
395
399
|
type = @type_inferrer.infer_receiver_type(@node_context)
|
|
396
|
-
|
|
400
|
+
nesting = type ? type.name.split("::") : []
|
|
397
401
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
variable_name = entry.name
|
|
402
|
+
@graph.complete_expression(nesting, self_receiver: nil).each do |candidate|
|
|
403
|
+
next unless candidate.is_a?(Rubydex::Declaration)
|
|
401
404
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
)
|
|
405
|
+
variable_name = candidate.unqualified_name
|
|
406
|
+
next unless variable_name.start_with?(prefix)
|
|
405
407
|
|
|
406
408
|
@response_builder << Interface::CompletionItem.new(
|
|
407
409
|
label: variable_name,
|
|
408
|
-
label_details:
|
|
409
|
-
|
|
410
|
-
range: range,
|
|
411
|
-
new_text: variable_name,
|
|
410
|
+
label_details: Interface::CompletionItemLabelDetails.new(
|
|
411
|
+
description: declaration_file_names(candidate),
|
|
412
412
|
),
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
},
|
|
413
|
+
text_edit: Interface::TextEdit.new(range: range, new_text: variable_name),
|
|
414
|
+
kind: candidate.to_lsp_completion_kind,
|
|
415
|
+
data: { owner_name: candidate.owner.name },
|
|
417
416
|
)
|
|
418
417
|
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
418
|
end
|
|
422
419
|
|
|
423
420
|
#: (Prism::CallNode node) -> void
|
|
@@ -429,13 +426,10 @@ module RubyLsp
|
|
|
429
426
|
|
|
430
427
|
return unless path_node_to_complete.is_a?(Prism::StringNode)
|
|
431
428
|
|
|
432
|
-
|
|
429
|
+
content = path_node_to_complete.content
|
|
433
430
|
|
|
434
|
-
|
|
435
|
-
@response_builder << build_completion(
|
|
436
|
-
path, #: as !nil
|
|
437
|
-
path_node_to_complete,
|
|
438
|
-
)
|
|
431
|
+
@graph.require_paths($LOAD_PATH).select { |path| path.start_with?(content) }.sort!.each do |path|
|
|
432
|
+
@response_builder << build_completion(path, path_node_to_complete)
|
|
439
433
|
end
|
|
440
434
|
end
|
|
441
435
|
|
|
@@ -474,120 +468,26 @@ module RubyLsp
|
|
|
474
468
|
# might fail with EPERM
|
|
475
469
|
end
|
|
476
470
|
|
|
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
|
-
)
|
|
471
|
+
#: (Rubydex::Method candidate, Interface::Range range, ?has_receiver: bool, ?guessed_type: (String | bool)) -> void
|
|
472
|
+
def add_method_completion(candidate, range, has_receiver: false, guessed_type: false)
|
|
473
|
+
display_name = candidate.unqualified_name.delete_suffix("()")
|
|
474
|
+
new_text = display_name
|
|
556
475
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
next unless local_name.start_with?(name)
|
|
476
|
+
if display_name.end_with?("=")
|
|
477
|
+
setter_name = display_name.delete_suffix("=")
|
|
560
478
|
|
|
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
|
-
)
|
|
479
|
+
# For writer methods, format as assignment and prefix "self." when no receiver is specified
|
|
480
|
+
new_text = has_receiver ? "#{setter_name} = " : "self.#{setter_name} = "
|
|
570
481
|
end
|
|
571
|
-
end
|
|
572
482
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
483
|
+
@response_builder << Interface::CompletionItem.new(
|
|
484
|
+
label: display_name,
|
|
485
|
+
filter_text: display_name,
|
|
486
|
+
label_details: Interface::CompletionItemLabelDetails.new(description: declaration_file_names(candidate)),
|
|
487
|
+
text_edit: Interface::TextEdit.new(range: range, new_text: new_text),
|
|
488
|
+
kind: candidate.to_lsp_completion_kind,
|
|
489
|
+
data: { owner_name: candidate.owner.name, guessed_type: guessed_type },
|
|
577
490
|
)
|
|
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
491
|
end
|
|
592
492
|
|
|
593
493
|
#: (String label, Prism::StringNode node) -> Interface::CompletionItem
|
|
@@ -605,68 +505,30 @@ module RubyLsp
|
|
|
605
505
|
)
|
|
606
506
|
end
|
|
607
507
|
|
|
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
|
-
|
|
508
|
+
#: (String real_name, String incomplete_name, Interface::Range range, Rubydex::Declaration declaration) -> Interface::CompletionItem
|
|
509
|
+
def build_entry_completion(real_name, incomplete_name, range, declaration)
|
|
622
510
|
insertion_text = real_name.dup
|
|
623
511
|
filter_text = real_name.dup
|
|
624
512
|
|
|
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)
|
|
513
|
+
# When the user explicitly typed `::Foo`, the absolute prefix must be preserved and the suffix-shortening
|
|
514
|
+
# below is skipped — replacing `::Bar` with `Bar` would change which constant resolves. The leading `::` is
|
|
515
|
+
# only present on `incomplete_name` (the user-typed text)
|
|
516
|
+
if incomplete_name.start_with?("::")
|
|
517
|
+
insertion_text.prepend("::") unless insertion_text.start_with?("::")
|
|
518
|
+
filter_text.prepend("::") unless filter_text.start_with?("::")
|
|
519
|
+
else
|
|
520
|
+
shortest = shortest_constant_suffix(real_name)
|
|
521
|
+
|
|
522
|
+
if shortest.length < insertion_text.length
|
|
523
|
+
stripped_prefix = real_name.delete_suffix(shortest)
|
|
524
|
+
insertion_text = shortest
|
|
525
|
+
# When the user is typing a more qualified path (e.g. `Foo::B`), keep the filter text qualified so the
|
|
526
|
+
# editor's filter still matches what they typed; otherwise the unqualified suffix is enough
|
|
527
|
+
filter_text = shortest unless incomplete_name.start_with?(stripped_prefix)
|
|
660
528
|
end
|
|
661
529
|
end
|
|
662
530
|
|
|
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
|
-
)
|
|
531
|
+
label_details = Interface::CompletionItemLabelDetails.new(description: declaration_file_names(declaration))
|
|
670
532
|
|
|
671
533
|
Interface::CompletionItem.new(
|
|
672
534
|
label: real_name,
|
|
@@ -676,37 +538,39 @@ module RubyLsp
|
|
|
676
538
|
range: range,
|
|
677
539
|
new_text: insertion_text,
|
|
678
540
|
),
|
|
679
|
-
kind:
|
|
541
|
+
kind: declaration.to_lsp_completion_kind,
|
|
542
|
+
data: { fully_qualified_name: declaration.name },
|
|
680
543
|
)
|
|
681
544
|
end
|
|
682
545
|
|
|
683
|
-
#
|
|
684
|
-
# For example:
|
|
546
|
+
# Returns the shortest possible name for a constant reference that still resolves to the same target
|
|
685
547
|
#
|
|
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)
|
|
548
|
+
#: (String) -> String
|
|
549
|
+
def shortest_constant_suffix(real_name)
|
|
550
|
+
segments = real_name.split("::")
|
|
699
551
|
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
552
|
|
|
706
|
-
|
|
553
|
+
(1..segments.length).each do |suffix_len|
|
|
554
|
+
suffix = segments.last(suffix_len).join("::")
|
|
555
|
+
resolved = @graph.resolve_constant(suffix, nesting)
|
|
556
|
+
return suffix if resolved && resolved.name == real_name
|
|
707
557
|
end
|
|
708
558
|
|
|
709
|
-
|
|
559
|
+
real_name
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
#: (Rubydex::Declaration declaration) -> String
|
|
563
|
+
def declaration_file_names(declaration)
|
|
564
|
+
declaration.definitions.filter_map do |defn|
|
|
565
|
+
uri = URI(defn.location.uri)
|
|
566
|
+
case uri.scheme
|
|
567
|
+
when "untitled"
|
|
568
|
+
uri.opaque
|
|
569
|
+
when "file"
|
|
570
|
+
path = uri.full_path
|
|
571
|
+
File.basename(path) if path
|
|
572
|
+
end
|
|
573
|
+
end.uniq.join(",")
|
|
710
574
|
end
|
|
711
575
|
end
|
|
712
576
|
end
|