pink_spoon 0.1.0
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 +7 -0
- data/bin/install-addon +34 -0
- data/bin/pink-spoon +8 -0
- data/lib/pink_spoon/constant_resolver.rb +469 -0
- data/lib/pink_spoon/definition_finder.rb +144 -0
- data/lib/pink_spoon/doc_extractor.rb +265 -0
- data/lib/pink_spoon/rbi_index.rb +334 -0
- data/lib/pink_spoon/server.rb +173 -0
- data/lib/pink_spoon/version.rb +5 -0
- data/lib/pink_spoon.rb +7 -0
- data/lib/ruby_lsp/pink_spoon/addon.rb +52 -0
- data/lib/ruby_lsp/pink_spoon/code_lens_listener.rb +57 -0
- data/lib/ruby_lsp/pink_spoon/completion_listener.rb +200 -0
- data/lib/ruby_lsp/pink_spoon/definition_listener.rb +425 -0
- data/lib/ruby_lsp/pink_spoon/hover_listener.rb +94 -0
- metadata +80 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module PinkSpoon
|
|
5
|
+
# Fired on every textDocument/definition request.
|
|
6
|
+
# Resolves the call under the cursor, looks up the RBI definition location,
|
|
7
|
+
# and pushes an Interface::Location into the response.
|
|
8
|
+
class DefinitionListener
|
|
9
|
+
def initialize(response_builder, uri, node_context, dispatcher, resolver, rbi_index, doc_extractor = nil)
|
|
10
|
+
@response_builder = response_builder
|
|
11
|
+
@uri = uri
|
|
12
|
+
@node_context = node_context
|
|
13
|
+
@resolver = resolver
|
|
14
|
+
@rbi_index = rbi_index
|
|
15
|
+
@doc_extractor = doc_extractor
|
|
16
|
+
|
|
17
|
+
dispatcher.register(
|
|
18
|
+
self,
|
|
19
|
+
:on_call_node_enter,
|
|
20
|
+
:on_local_variable_read_node_enter,
|
|
21
|
+
:on_constant_read_node_enter,
|
|
22
|
+
:on_constant_path_node_enter,
|
|
23
|
+
:on_instance_variable_read_node_enter,
|
|
24
|
+
:on_forwarding_super_node_enter,
|
|
25
|
+
:on_super_node_enter,
|
|
26
|
+
:on_symbol_node_enter,
|
|
27
|
+
:on_def_node_enter,
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def on_call_node_enter(node)
|
|
32
|
+
return unless node.equal?(@node_context.node)
|
|
33
|
+
|
|
34
|
+
nesting = @node_context.instance_variable_get(:@nesting_nodes)
|
|
35
|
+
program_node = nesting&.find { |n| n.is_a?(Prism::ProgramNode) }
|
|
36
|
+
return unless program_node
|
|
37
|
+
|
|
38
|
+
resolved = @resolver.resolve_from_call(node, program_node, nesting)
|
|
39
|
+
|
|
40
|
+
location = if resolved
|
|
41
|
+
source_location(resolved) || rbi_location(resolved)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Bare call (no explicit receiver): try two more strategies.
|
|
45
|
+
if location.nil? && node.receiver.nil?
|
|
46
|
+
method_name = node.name.to_s
|
|
47
|
+
location = nesting_location(method_name, nesting) ||
|
|
48
|
+
current_file_location(method_name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Fallback: ruby-lsp resolves LocalVariableReadNode receivers up to the
|
|
52
|
+
# parent CallNode (LocalVariableReadNode is not in the definition node_types
|
|
53
|
+
# list). If method resolution failed, navigate to the receiver variable's
|
|
54
|
+
# declaration instead — that is almost certainly what the user clicked on.
|
|
55
|
+
if location.nil? && node.receiver
|
|
56
|
+
location = receiver_variable_location(node.receiver)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return unless location
|
|
60
|
+
|
|
61
|
+
push_location(location[:file], location[:line])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def on_constant_read_node_enter(node)
|
|
65
|
+
return unless node.equal?(@node_context.node)
|
|
66
|
+
goto_constant(node.name.to_s)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def on_constant_path_node_enter(node)
|
|
70
|
+
return unless node.equal?(@node_context.node)
|
|
71
|
+
goto_constant(node.slice.delete_prefix("::"))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Symbol go-to-def: :method_name → def method_name
|
|
75
|
+
def on_symbol_node_enter(node)
|
|
76
|
+
return unless node.equal?(@node_context.node)
|
|
77
|
+
|
|
78
|
+
method_name = node.value
|
|
79
|
+
return unless method_name && !method_name.empty?
|
|
80
|
+
|
|
81
|
+
loc = current_file_location(method_name)
|
|
82
|
+
return push_location(loc[:file], loc[:line]) if loc
|
|
83
|
+
|
|
84
|
+
loc = @doc_extractor&.find_source(
|
|
85
|
+
guess_type_from_nesting(@node_context.instance_variable_get(:@nesting_nodes)),
|
|
86
|
+
method_name
|
|
87
|
+
)
|
|
88
|
+
push_location(loc[:file], loc[:line]) if loc
|
|
89
|
+
rescue
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Hover on def: cursor on the method name in a definition
|
|
94
|
+
def on_def_node_enter(node)
|
|
95
|
+
return unless node.equal?(@node_context.node)
|
|
96
|
+
|
|
97
|
+
nesting = @node_context.instance_variable_get(:@nesting_nodes)
|
|
98
|
+
class_node = nesting&.reverse&.find { |n| n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode) }
|
|
99
|
+
return unless class_node
|
|
100
|
+
|
|
101
|
+
type = class_node.constant_path.slice.delete_prefix("::")
|
|
102
|
+
loc = source_location({ type: type, method: node.name.to_s })
|
|
103
|
+
push_location(loc[:file], loc[:line]) if loc
|
|
104
|
+
rescue
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def on_forwarding_super_node_enter(node)
|
|
109
|
+
return unless node.equal?(@node_context.node)
|
|
110
|
+
navigate_super(node)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def on_super_node_enter(node)
|
|
114
|
+
return unless node.equal?(@node_context.node)
|
|
115
|
+
navigate_super(node)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def on_instance_variable_read_node_enter(node)
|
|
119
|
+
return unless node.equal?(@node_context.node)
|
|
120
|
+
|
|
121
|
+
file = @uri.to_s.delete_prefix("file://")
|
|
122
|
+
return unless file && File.exist?(file)
|
|
123
|
+
|
|
124
|
+
ivar_name = node.name.to_s
|
|
125
|
+
read_line = node.location.start_line
|
|
126
|
+
|
|
127
|
+
result = Prism.parse_file(file)
|
|
128
|
+
finder = IvarAssignmentFinder.new(ivar_name, read_line)
|
|
129
|
+
finder.visit(result.value)
|
|
130
|
+
|
|
131
|
+
if finder.line
|
|
132
|
+
push_location(file, finder.line)
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Not found in current file — walk the parent class chain.
|
|
137
|
+
nesting = @node_context.instance_variable_get(:@nesting_nodes)
|
|
138
|
+
class_node = nesting&.reverse&.find { |n| n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode) }
|
|
139
|
+
return unless class_node
|
|
140
|
+
|
|
141
|
+
current_type = class_node.constant_path.slice.delete_prefix("::")
|
|
142
|
+
@rbi_index.mixins_for(current_type).each do |parent_type|
|
|
143
|
+
loc = @doc_extractor&.find_ivar_in_type(parent_type, ivar_name)
|
|
144
|
+
if loc
|
|
145
|
+
push_location(loc[:file], loc[:line])
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
rescue
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def navigate_super(node)
|
|
156
|
+
nesting = @node_context.instance_variable_get(:@nesting_nodes)
|
|
157
|
+
program_node = nesting&.find { |n| n.is_a?(Prism::ProgramNode) }
|
|
158
|
+
return unless program_node
|
|
159
|
+
|
|
160
|
+
class_node = nesting.reverse.find { |n| n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode) }
|
|
161
|
+
return unless class_node
|
|
162
|
+
|
|
163
|
+
current_type = class_node.constant_path.slice.delete_prefix("::")
|
|
164
|
+
method_name = EnclosingMethodFinder.new(node.location.start_line).find(program_node)
|
|
165
|
+
return unless method_name
|
|
166
|
+
|
|
167
|
+
@rbi_index.mixins_for(current_type).each do |parent_type|
|
|
168
|
+
resolved = { type: parent_type, method: method_name }
|
|
169
|
+
location = rbi_location(resolved) || source_location(resolved)
|
|
170
|
+
if location
|
|
171
|
+
push_location(location[:file], location[:line])
|
|
172
|
+
return
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# When go-to-def is clicked on a local variable or ivar that is the
|
|
178
|
+
# receiver of a method call, navigate to where that variable is declared.
|
|
179
|
+
def receiver_variable_location(receiver)
|
|
180
|
+
file = @uri.to_s.delete_prefix("file://")
|
|
181
|
+
return nil unless file && File.exist?(file)
|
|
182
|
+
|
|
183
|
+
result = Prism.parse_file(file)
|
|
184
|
+
|
|
185
|
+
case receiver
|
|
186
|
+
when Prism::LocalVariableReadNode
|
|
187
|
+
finder = LocalVariableDefinitionFinder.new(
|
|
188
|
+
receiver.name.to_s,
|
|
189
|
+
receiver.location.start_line,
|
|
190
|
+
)
|
|
191
|
+
finder.visit(result.value)
|
|
192
|
+
{ file: file, line: finder.line } if finder.line
|
|
193
|
+
|
|
194
|
+
when Prism::InstanceVariableReadNode
|
|
195
|
+
finder = IvarAssignmentFinder.new(
|
|
196
|
+
receiver.name.to_s,
|
|
197
|
+
receiver.location.start_line,
|
|
198
|
+
)
|
|
199
|
+
finder.visit(result.value)
|
|
200
|
+
{ file: file, line: finder.line } if finder.line
|
|
201
|
+
end
|
|
202
|
+
rescue
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def guess_type_from_nesting(nesting)
|
|
207
|
+
class_node = nesting&.reverse&.find { |n| n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode) }
|
|
208
|
+
class_node&.constant_path&.slice&.delete_prefix("::")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def goto_constant(type)
|
|
212
|
+
location = @doc_extractor&.find_constant_source(type)
|
|
213
|
+
return unless location
|
|
214
|
+
push_location(location[:file], location[:line])
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def rbi_location(resolved)
|
|
218
|
+
@rbi_index.rbi_location_for(resolved[:type], resolved[:method])
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def source_location(resolved)
|
|
222
|
+
@doc_extractor&.find_source(resolved[:type], resolved[:method])
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Infer the type from the innermost enclosing class/module in the nesting
|
|
226
|
+
# and look the method up in the RBI index or gem source.
|
|
227
|
+
def nesting_location(method_name, nesting)
|
|
228
|
+
return nil unless nesting
|
|
229
|
+
|
|
230
|
+
class_node = nesting.reverse.find do |n|
|
|
231
|
+
n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode)
|
|
232
|
+
end
|
|
233
|
+
return nil unless class_node
|
|
234
|
+
|
|
235
|
+
type = class_node.constant_path.slice.delete_prefix("::")
|
|
236
|
+
resolved = { type: type, method: method_name }
|
|
237
|
+
|
|
238
|
+
rbi_location(resolved) || source_location(resolved)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Handles go-to-definition for real local variables (entry = ..., |entry|, etc).
|
|
242
|
+
def on_local_variable_read_node_enter(node)
|
|
243
|
+
return unless node.equal?(@node_context.node)
|
|
244
|
+
|
|
245
|
+
file = @uri.to_s.delete_prefix("file://")
|
|
246
|
+
return unless file && File.exist?(file)
|
|
247
|
+
|
|
248
|
+
var_name = node.name.to_s
|
|
249
|
+
read_line = node.location.start_line
|
|
250
|
+
|
|
251
|
+
result = Prism.parse_file(file)
|
|
252
|
+
finder = LocalVariableDefinitionFinder.new(var_name, read_line)
|
|
253
|
+
finder.visit(result.value)
|
|
254
|
+
line = finder.line
|
|
255
|
+
return unless line
|
|
256
|
+
|
|
257
|
+
push_location(file, line)
|
|
258
|
+
rescue
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Last resort: scan the file the user is currently editing.
|
|
263
|
+
# Matches def, let(:name), let!(:name), subject(:name), subject!(:name).
|
|
264
|
+
def current_file_location(method_name)
|
|
265
|
+
file = @uri.to_s.delete_prefix("file://")
|
|
266
|
+
return nil unless file && File.exist?(file)
|
|
267
|
+
|
|
268
|
+
e = Regexp.escape(method_name)
|
|
269
|
+
def_re = /\bdef\s+(?:self\.)?#{e}(?=[\s\(;]|\z)/
|
|
270
|
+
let_re = /\blet[!]?\s*\(\s*:#{e}\b/
|
|
271
|
+
subj_re = /\bsubject[!]?\s*\(\s*:#{e}\b/
|
|
272
|
+
|
|
273
|
+
File.foreach(file).with_index(1) do |raw, i|
|
|
274
|
+
return { file: file, line: i } if raw.match?(def_re) || raw.match?(let_re) || raw.match?(subj_re)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
nil
|
|
278
|
+
rescue
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def push_location(file, line)
|
|
283
|
+
line_lsp = line - 1
|
|
284
|
+
@response_builder << Interface::Location.new(
|
|
285
|
+
uri: "file://#{file}",
|
|
286
|
+
range: Interface::Range.new(
|
|
287
|
+
start: Interface::Position.new(line: line_lsp, character: 0),
|
|
288
|
+
end: Interface::Position.new(line: line_lsp, character: 0),
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Finds the line where a local variable was last assigned before
|
|
294
|
+
# a given line. Handles plain writes, block params, and method params.
|
|
295
|
+
class LocalVariableDefinitionFinder < Prism::Visitor
|
|
296
|
+
attr_reader :line
|
|
297
|
+
|
|
298
|
+
def initialize(var_name, before_line)
|
|
299
|
+
@var_name = var_name
|
|
300
|
+
@before_line = before_line
|
|
301
|
+
@candidates = []
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def line
|
|
305
|
+
@candidates.last
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def visit_local_variable_write_node(node)
|
|
309
|
+
record(node)
|
|
310
|
+
super
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def visit_local_variable_operator_write_node(node)
|
|
314
|
+
record(node)
|
|
315
|
+
super
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def visit_local_variable_or_write_node(node)
|
|
319
|
+
record(node)
|
|
320
|
+
super
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def visit_local_variable_and_write_node(node)
|
|
324
|
+
record(node)
|
|
325
|
+
super
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Method parameters: def foo(entry)
|
|
329
|
+
def visit_required_parameter_node(node)
|
|
330
|
+
record(node)
|
|
331
|
+
super
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def visit_optional_parameter_node(node)
|
|
335
|
+
record(node)
|
|
336
|
+
super
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Block parameters: do |entry|
|
|
340
|
+
def visit_block_parameter_node(node)
|
|
341
|
+
record(node)
|
|
342
|
+
super
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def visit_required_keyword_parameter_node(node)
|
|
346
|
+
record(node)
|
|
347
|
+
super
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def visit_optional_keyword_parameter_node(node)
|
|
351
|
+
record(node)
|
|
352
|
+
super
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
private
|
|
356
|
+
|
|
357
|
+
def record(node)
|
|
358
|
+
return unless node.name.to_s == @var_name
|
|
359
|
+
line_no = node.location.start_line
|
|
360
|
+
@candidates << line_no if line_no <= @before_line
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Finds the name of the innermost method def that contains a target line.
|
|
365
|
+
class EnclosingMethodFinder < Prism::Visitor
|
|
366
|
+
def initialize(target_line)
|
|
367
|
+
@target_line = target_line
|
|
368
|
+
@best_name = nil
|
|
369
|
+
@best_span = nil
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def find(ast)
|
|
373
|
+
visit(ast)
|
|
374
|
+
@best_name
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def visit_def_node(node)
|
|
378
|
+
loc = node.location
|
|
379
|
+
return super unless @target_line >= loc.start_line && @target_line <= loc.end_line
|
|
380
|
+
|
|
381
|
+
span = loc.end_line - loc.start_line
|
|
382
|
+
if @best_span.nil? || span < @best_span
|
|
383
|
+
@best_span = span
|
|
384
|
+
@best_name = node.name.to_s
|
|
385
|
+
end
|
|
386
|
+
super
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Finds the nearest instance variable write (@ivar = ...) before a given line.
|
|
391
|
+
class IvarAssignmentFinder < Prism::Visitor
|
|
392
|
+
def initialize(ivar_name, before_line)
|
|
393
|
+
@ivar_name = ivar_name
|
|
394
|
+
@before_line = before_line
|
|
395
|
+
@candidates = []
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def line
|
|
399
|
+
@candidates.last
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def visit_instance_variable_write_node(node)
|
|
403
|
+
if node.name.to_s == @ivar_name && node.location.start_line <= @before_line
|
|
404
|
+
@candidates << node.location.start_line
|
|
405
|
+
end
|
|
406
|
+
super
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def visit_instance_variable_operator_write_node(node)
|
|
410
|
+
if node.name.to_s == @ivar_name && node.location.start_line <= @before_line
|
|
411
|
+
@candidates << node.location.start_line
|
|
412
|
+
end
|
|
413
|
+
super
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def visit_instance_variable_or_write_node(node)
|
|
417
|
+
if node.name.to_s == @ivar_name && node.location.start_line <= @before_line
|
|
418
|
+
@candidates << node.location.start_line
|
|
419
|
+
end
|
|
420
|
+
super
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLsp
|
|
4
|
+
module PinkSpoon
|
|
5
|
+
# Fired on every textDocument/hover request.
|
|
6
|
+
# Registers for two node types:
|
|
7
|
+
# on_program_node_enter — captures the root AST so AssignmentFinder can
|
|
8
|
+
# scan the whole file for constant assignments.
|
|
9
|
+
# on_call_node_enter — resolves the receiver type for the call under
|
|
10
|
+
# the cursor and pushes a sig string into the
|
|
11
|
+
# hover panel.
|
|
12
|
+
class HoverListener
|
|
13
|
+
def initialize(response_builder, node_context, dispatcher, resolver, rbi_index, doc_extractor = nil)
|
|
14
|
+
@response_builder = response_builder
|
|
15
|
+
@node_context = node_context
|
|
16
|
+
@resolver = resolver
|
|
17
|
+
@rbi_index = rbi_index
|
|
18
|
+
@doc_extractor = doc_extractor
|
|
19
|
+
|
|
20
|
+
dispatcher.register(
|
|
21
|
+
self,
|
|
22
|
+
:on_call_node_enter,
|
|
23
|
+
:on_constant_read_node_enter,
|
|
24
|
+
:on_constant_path_node_enter,
|
|
25
|
+
:on_instance_variable_read_node_enter,
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def on_constant_read_node_enter(node)
|
|
30
|
+
return unless node.equal?(@node_context.node)
|
|
31
|
+
serve_constant_hover(node.name.to_s)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def on_constant_path_node_enter(node)
|
|
35
|
+
return unless node.equal?(@node_context.node)
|
|
36
|
+
serve_constant_hover(node.slice.delete_prefix("::"))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def on_instance_variable_read_node_enter(node)
|
|
40
|
+
return unless node.equal?(@node_context.node)
|
|
41
|
+
|
|
42
|
+
nesting = @node_context.instance_variable_get(:@nesting_nodes)
|
|
43
|
+
class_node = nesting&.reverse&.find { |n| n.is_a?(Prism::ClassNode) || n.is_a?(Prism::ModuleNode) }
|
|
44
|
+
return unless class_node
|
|
45
|
+
|
|
46
|
+
type = class_node.constant_path.slice.delete_prefix("::")
|
|
47
|
+
accessor = node.name.to_s.delete_prefix("@")
|
|
48
|
+
|
|
49
|
+
content = @rbi_index.hover_content_for(type, accessor)
|
|
50
|
+
return unless content
|
|
51
|
+
|
|
52
|
+
@response_builder.push(content, category: :documentation)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def on_call_node_enter(node)
|
|
56
|
+
return unless node.equal?(@node_context.node)
|
|
57
|
+
|
|
58
|
+
nesting = @node_context.instance_variable_get(:@nesting_nodes)
|
|
59
|
+
program_node = nesting&.find { |n| n.is_a?(Prism::ProgramNode) }
|
|
60
|
+
return unless program_node
|
|
61
|
+
|
|
62
|
+
resolved = @resolver.resolve_from_call(node, program_node)
|
|
63
|
+
return unless resolved
|
|
64
|
+
|
|
65
|
+
rbi_content = @rbi_index.hover_content_for(resolved[:type], resolved[:method])
|
|
66
|
+
doc_content = @doc_extractor&.extract(resolved[:type], resolved[:method])
|
|
67
|
+
|
|
68
|
+
content = combine(rbi_content, doc_content)
|
|
69
|
+
return unless content
|
|
70
|
+
|
|
71
|
+
@response_builder.push(content, category: :documentation)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def serve_constant_hover(type)
|
|
77
|
+
return unless @doc_extractor
|
|
78
|
+
|
|
79
|
+
doc = @doc_extractor.extract_for_constant(type)
|
|
80
|
+
return unless doc
|
|
81
|
+
|
|
82
|
+
@response_builder.push("**`#{type}`**\n\n#{doc}", category: :documentation)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def combine(rbi, doc)
|
|
86
|
+
return nil unless rbi || doc
|
|
87
|
+
return rbi unless doc
|
|
88
|
+
return doc unless rbi
|
|
89
|
+
|
|
90
|
+
"#{rbi}\n\n---\n\n#{doc}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pink_spoon
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jānis Harbs
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-06-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: prism
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.24'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.24'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ruby-lsp
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.22'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.22'
|
|
40
|
+
description: Augments ruby-lsp hover and go-to-definition with type information from
|
|
41
|
+
sorbet/rbi/**/*.rbi. Drop it in your Gemfile and it just works.
|
|
42
|
+
executables:
|
|
43
|
+
- pink-spoon
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- bin/install-addon
|
|
48
|
+
- bin/pink-spoon
|
|
49
|
+
- lib/pink_spoon.rb
|
|
50
|
+
- lib/pink_spoon/constant_resolver.rb
|
|
51
|
+
- lib/pink_spoon/definition_finder.rb
|
|
52
|
+
- lib/pink_spoon/doc_extractor.rb
|
|
53
|
+
- lib/pink_spoon/rbi_index.rb
|
|
54
|
+
- lib/pink_spoon/server.rb
|
|
55
|
+
- lib/pink_spoon/version.rb
|
|
56
|
+
- lib/ruby_lsp/pink_spoon/addon.rb
|
|
57
|
+
- lib/ruby_lsp/pink_spoon/code_lens_listener.rb
|
|
58
|
+
- lib/ruby_lsp/pink_spoon/completion_listener.rb
|
|
59
|
+
- lib/ruby_lsp/pink_spoon/definition_listener.rb
|
|
60
|
+
- lib/ruby_lsp/pink_spoon/hover_listener.rb
|
|
61
|
+
licenses: []
|
|
62
|
+
metadata: {}
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '3.1'
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 3.6.2
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: 'ruby-lsp addon: resolves types from Sorbet RBI files'
|
|
80
|
+
test_files: []
|