pink_spoon 0.1.3 → 0.1.4
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/lib/pink_spoon/constant_resolver.rb +28 -2
- data/lib/pink_spoon/doc_extractor.rb +118 -20
- data/lib/pink_spoon/rbi_index.rb +9 -0
- data/lib/pink_spoon/version.rb +1 -1
- data/lib/ruby_lsp/pink_spoon/completion_listener.rb +20 -1
- data/lib/ruby_lsp/pink_spoon/definition_listener.rb +88 -1
- data/lib/ruby_lsp/pink_spoon/hover_listener.rb +6 -0
- metadata +3 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b276ba7405e08362bb430d60c37fc1620923b76c64da8484d08108ac6206e18
|
|
4
|
+
data.tar.gz: 31f275f9b72e231be93b8d9f9377496bd507b2e40d7dd1c6eb6c85eda80855d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8ba5f82e91947b993ae5784281518a2dc2c2685c2ecb6fd931eb85e56ae5342081d47457a9f31ce35b8ef2df4905f4cabd50974246004c443bea8af98b9b19c
|
|
7
|
+
data.tar.gz: 1dfcdad69156b586eebdaa38f4d5aba482c43f8cb3cc1f5cb906ff6e7727cdc02132d909003f05a9ac3ce557ddc06538bd0549ae16ce14b580b573df7bf9ea3e
|
|
@@ -18,6 +18,24 @@ module PinkSpoon
|
|
|
18
18
|
class ConstantResolver
|
|
19
19
|
PASSTHROUGH_METHODS = %w[freeze dup clone tap then yield_self].freeze
|
|
20
20
|
|
|
21
|
+
# These always return String regardless of receiver type.
|
|
22
|
+
UNIVERSAL_STRING_METHODS = %w[to_s to_str inspect].freeze
|
|
23
|
+
|
|
24
|
+
# String methods that return String when called on a String receiver.
|
|
25
|
+
# Used as a fallback when no RBI data is available for the core library.
|
|
26
|
+
STRING_SELF_RETURNING_METHODS = %w[
|
|
27
|
+
tr tr_s encode force_encoding b
|
|
28
|
+
gsub sub gsub! sub!
|
|
29
|
+
strip lstrip rstrip strip! lstrip! rstrip!
|
|
30
|
+
chomp chop chomp! chop!
|
|
31
|
+
upcase downcase swapcase capitalize
|
|
32
|
+
upcase! downcase! swapcase! capitalize!
|
|
33
|
+
reverse reverse! squeeze delete scrub
|
|
34
|
+
unicode_normalize unicode_normalize!
|
|
35
|
+
replace insert prepend center ljust rjust
|
|
36
|
+
slice [] + * %
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
21
39
|
RSPEC_EXAMPLE_GROUP_METHODS = %w[
|
|
22
40
|
it example specify
|
|
23
41
|
context describe
|
|
@@ -250,11 +268,19 @@ module PinkSpoon
|
|
|
250
268
|
|
|
251
269
|
def follow_chain(type, method_name)
|
|
252
270
|
return type if PASSTHROUGH_METHODS.include?(method_name)
|
|
271
|
+
return "String" if UNIVERSAL_STRING_METHODS.include?(method_name)
|
|
253
272
|
return nil unless type
|
|
254
273
|
|
|
255
274
|
clean = unwrap_sorbet_wrapper(type)
|
|
256
|
-
|
|
257
|
-
|
|
275
|
+
# Strip generic parameters before RBI lookup: T::Array[Account] → T::Array
|
|
276
|
+
base = clean.sub(/\[.*\]\z/, "")
|
|
277
|
+
ret = @rbi_index.return_type_for(base, method_name)&.delete_prefix("::")
|
|
278
|
+
return unwrap_sorbet_wrapper(ret) if ret
|
|
279
|
+
|
|
280
|
+
# Fall back to known String method return types when RBI data is absent.
|
|
281
|
+
return base if base == "String" && STRING_SELF_RETURNING_METHODS.include?(method_name)
|
|
282
|
+
|
|
283
|
+
nil
|
|
258
284
|
end
|
|
259
285
|
|
|
260
286
|
def unwrap_sorbet_wrapper(type)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
3
5
|
module PinkSpoon
|
|
4
6
|
# Fetches YARD/RDoc comments from installed gem source files for a given
|
|
5
7
|
# type + method. Handles both regular `def` and DSL-style definitions like
|
|
@@ -81,6 +83,22 @@ module PinkSpoon
|
|
|
81
83
|
nil
|
|
82
84
|
end
|
|
83
85
|
|
|
86
|
+
# Returns hover markdown for a known file+line (used as fallback when
|
|
87
|
+
# find_constant_location cannot locate the constant via gem-hint scanning).
|
|
88
|
+
def extract_for_location(loc)
|
|
89
|
+
return nil unless loc
|
|
90
|
+
|
|
91
|
+
raw = read_comments(loc[:file], loc[:line])
|
|
92
|
+
if raw.empty?
|
|
93
|
+
decl = declaration_line(loc[:file], loc[:line])
|
|
94
|
+
decl ? "```ruby\n#{decl}\n```" : nil
|
|
95
|
+
else
|
|
96
|
+
render(raw)
|
|
97
|
+
end
|
|
98
|
+
rescue
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
84
102
|
# Returns { file: absolute_path, line: integer } or nil.
|
|
85
103
|
def find_source(type, method_name)
|
|
86
104
|
cache_key = "loc:#{type}##{method_name}"
|
|
@@ -101,6 +119,8 @@ module PinkSpoon
|
|
|
101
119
|
if loc
|
|
102
120
|
raw = read_comments(loc[:file], loc[:line])
|
|
103
121
|
raw.empty? ? nil : render(raw)
|
|
122
|
+
else
|
|
123
|
+
extract_via_ri(type, method_name)
|
|
104
124
|
end
|
|
105
125
|
end
|
|
106
126
|
rescue
|
|
@@ -114,45 +134,52 @@ module PinkSpoon
|
|
|
114
134
|
name = parts.last
|
|
115
135
|
hint = parts.first.to_s.downcase
|
|
116
136
|
escaped = Regexp.escape(name)
|
|
137
|
+
target = parts.join("::")
|
|
117
138
|
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
# Gem files: fast single-line regex scan.
|
|
140
|
+
# Gem source is usually well-structured (one class per file, inline namespaces),
|
|
141
|
+
# so the regex false-positive risk is low and parsing every gem file is too slow.
|
|
142
|
+
gem_pattern = /^\s*(?:(?:class|module)\s+(?:\w+::)*#{escaped}\b|#{escaped}\s*=)/
|
|
122
143
|
gem_dirs_for(hint).each do |gem_dir|
|
|
123
144
|
Dir.glob("#{gem_dir}/lib/**/*.rb").sort.each do |file|
|
|
124
|
-
line = first_line_matching(file,
|
|
145
|
+
line = first_line_matching(file, gem_pattern)
|
|
125
146
|
return { file: file, line: line } if line
|
|
126
147
|
end
|
|
127
148
|
end
|
|
128
149
|
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
150
|
+
# Project files: parse with Prism so we track the fully-qualified name.
|
|
151
|
+
# A regex on individual lines cannot tell whether `module Restrictions` is
|
|
152
|
+
# top-level or nested inside `module Routes { module Restrictions }` — only
|
|
153
|
+
# a full parse can distinguish them.
|
|
154
|
+
prefilter = /\b#{escaped}\b/
|
|
155
|
+
|
|
133
156
|
%w[lib app spec test].each do |subdir|
|
|
134
157
|
dir = File.join(@root_path, subdir)
|
|
135
158
|
next unless File.directory?(dir)
|
|
136
159
|
|
|
137
160
|
if parts.size > 1
|
|
161
|
+
# Zeitwerk conventional path: try the direct file first.
|
|
138
162
|
candidate = File.join(dir, *parts.map { |p| underscore(p) }) + ".rb"
|
|
139
163
|
if File.exist?(candidate)
|
|
140
|
-
line =
|
|
164
|
+
line = exact_constant_line(candidate, target)
|
|
141
165
|
return { file: candidate, line: line || 1 } if line
|
|
142
166
|
end
|
|
143
|
-
# Glob fallback
|
|
144
|
-
#
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
167
|
+
# Glob fallback with progressive namespace prefix shortening.
|
|
168
|
+
# Full prefix first preserves false-positive safety for deep hierarchies.
|
|
169
|
+
all_files = Dir.glob("#{dir}/**/*.rb").sort
|
|
170
|
+
(parts.size - 1).downto(1) do |depth|
|
|
171
|
+
intermediate = parts[0...depth].map { |p| underscore(p) }.join("/")
|
|
172
|
+
all_files.each do |file|
|
|
173
|
+
next unless file.include?("/#{intermediate}/") || file.end_with?("/#{intermediate}.rb")
|
|
174
|
+
next unless first_line_matching(file, prefilter)
|
|
175
|
+
line = exact_constant_line(file, target)
|
|
176
|
+
return { file: file, line: line } if line
|
|
177
|
+
end
|
|
152
178
|
end
|
|
153
179
|
else
|
|
154
180
|
Dir.glob("#{dir}/**/*.rb").sort.each do |file|
|
|
155
|
-
|
|
181
|
+
next unless first_line_matching(file, prefilter)
|
|
182
|
+
line = exact_constant_line(file, target)
|
|
156
183
|
return { file: file, line: line } if line
|
|
157
184
|
end
|
|
158
185
|
end
|
|
@@ -161,6 +188,21 @@ module PinkSpoon
|
|
|
161
188
|
nil
|
|
162
189
|
end
|
|
163
190
|
|
|
191
|
+
# Returns the line number where +target+ (fully-qualified, e.g. "Restrictions"
|
|
192
|
+
# or "Routes::Restrictions") is defined in +file+, or nil if not found.
|
|
193
|
+
# Parses the file with Prism and tracks the scope stack so that a nested
|
|
194
|
+
# `module Restrictions` inside `module Routes` is NOT mistaken for the
|
|
195
|
+
# top-level `Restrictions` module.
|
|
196
|
+
def exact_constant_line(file, target)
|
|
197
|
+
source = File.read(file)
|
|
198
|
+
result = Prism.parse(source)
|
|
199
|
+
finder = ConstantDefinitionFinder.new(target)
|
|
200
|
+
finder.visit(result.value)
|
|
201
|
+
finder.line
|
|
202
|
+
rescue
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
164
206
|
def underscore(str)
|
|
165
207
|
str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
166
208
|
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
@@ -224,7 +266,11 @@ module PinkSpoon
|
|
|
224
266
|
nil
|
|
225
267
|
end
|
|
226
268
|
|
|
269
|
+
DIRECTIVE_RE = /^#\s*(?:rubocop:|frozen_string_literal:|typed:|encoding:|source:\/\/)/.freeze
|
|
270
|
+
|
|
227
271
|
# Reads the contiguous comment block immediately above line_no (1-based).
|
|
272
|
+
# Stops at non-comment lines and at directive comments (rubocop, sorbet typed,
|
|
273
|
+
# frozen_string_literal, encoding, source://) which are never documentation.
|
|
228
274
|
def read_comments(file, line_no)
|
|
229
275
|
lines = File.readlines(file, chomp: true)
|
|
230
276
|
block = []
|
|
@@ -233,6 +279,7 @@ module PinkSpoon
|
|
|
233
279
|
while i >= 0
|
|
234
280
|
stripped = lines[i].strip
|
|
235
281
|
break unless stripped.start_with?("#")
|
|
282
|
+
break if stripped.match?(DIRECTIVE_RE)
|
|
236
283
|
block.unshift(stripped.sub(/^#[ \t]?/, ""))
|
|
237
284
|
i -= 1
|
|
238
285
|
end
|
|
@@ -250,6 +297,20 @@ module PinkSpoon
|
|
|
250
297
|
[]
|
|
251
298
|
end
|
|
252
299
|
|
|
300
|
+
# Fetches documentation for core Ruby types (String, Integer, etc.) via the
|
|
301
|
+
# `ri` tool. Used when `find_location` returns nil because the type has no
|
|
302
|
+
# gem source (built-in C extension methods like String#tr, String#encode).
|
|
303
|
+
def extract_via_ri(type, method_name)
|
|
304
|
+
out, _err, status = Open3.capture3("ri", "--no-pager", "--format=markdown", "#{type}##{method_name}")
|
|
305
|
+
return nil unless status.success?
|
|
306
|
+
content = out.strip
|
|
307
|
+
return nil if content.empty? || content.include?("Nothing known about")
|
|
308
|
+
# Strip the "# Type#method" title — the hover panel renders its own header.
|
|
309
|
+
content.sub(/\A#[^\n]+\n+/, "").strip.then { |s| s.empty? ? nil : s }
|
|
310
|
+
rescue
|
|
311
|
+
nil
|
|
312
|
+
end
|
|
313
|
+
|
|
253
314
|
YARD_TAG_RE = /^@(\w+)(?:\s+(.*))?$/.freeze
|
|
254
315
|
|
|
255
316
|
# Converts a raw comment array into a markdown string.
|
|
@@ -289,5 +350,42 @@ module PinkSpoon
|
|
|
289
350
|
result = output.join("\n").strip
|
|
290
351
|
result.empty? ? nil : result
|
|
291
352
|
end
|
|
353
|
+
|
|
354
|
+
# Walks a Prism AST and records the line where +target+ is defined.
|
|
355
|
+
# Tracks the full qualified name via a scope stack, so a nested
|
|
356
|
+
# `module Restrictions` inside `module Routes` only matches
|
|
357
|
+
# "Routes::Restrictions", never bare "Restrictions".
|
|
358
|
+
class ConstantDefinitionFinder < Prism::Visitor
|
|
359
|
+
attr_reader :line
|
|
360
|
+
|
|
361
|
+
def initialize(target)
|
|
362
|
+
@target = target.delete_prefix("::")
|
|
363
|
+
@scope = []
|
|
364
|
+
@line = nil
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def visit_module_node(node)
|
|
368
|
+
push_scope(node.constant_path.slice.delete_prefix("::"), node.location.start_line) { super }
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def visit_class_node(node)
|
|
372
|
+
push_scope(node.constant_path.slice.delete_prefix("::"), node.location.start_line) { super }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def visit_constant_write_node(node)
|
|
376
|
+
fqn = (@scope + [node.name.to_s]).join("::")
|
|
377
|
+
@line ||= node.location.start_line if fqn == @target
|
|
378
|
+
super
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
private
|
|
382
|
+
|
|
383
|
+
def push_scope(name, start_line)
|
|
384
|
+
@scope.push(name)
|
|
385
|
+
@line ||= start_line if @scope.join("::") == @target
|
|
386
|
+
yield
|
|
387
|
+
@scope.pop
|
|
388
|
+
end
|
|
389
|
+
end
|
|
292
390
|
end
|
|
293
391
|
end
|
data/lib/pink_spoon/rbi_index.rb
CHANGED
|
@@ -332,6 +332,15 @@ module PinkSpoon
|
|
|
332
332
|
super
|
|
333
333
|
end
|
|
334
334
|
|
|
335
|
+
def visit_constant_write_node(node)
|
|
336
|
+
source_comment = source_comment_at(node.location.start_line)
|
|
337
|
+
if source_comment
|
|
338
|
+
full_name = (@scope + [node.name.to_s]).join("::")
|
|
339
|
+
@const_sources[full_name] = source_comment
|
|
340
|
+
end
|
|
341
|
+
super
|
|
342
|
+
end
|
|
343
|
+
|
|
335
344
|
def visit_def_node(node)
|
|
336
345
|
method_name = node.name.to_s
|
|
337
346
|
sig = @pending_sig
|
data/lib/pink_spoon/version.rb
CHANGED
|
@@ -36,6 +36,10 @@ module RubyLsp
|
|
|
36
36
|
INSERT_FORMAT_SNIPPET = 2
|
|
37
37
|
INSERT_FORMAT_PLAINTEXT = 1
|
|
38
38
|
|
|
39
|
+
# Methods universal to all Ruby objects — excluded from core-type completions
|
|
40
|
+
# so we don't flood the list with nil?, frozen?, object_id, etc.
|
|
41
|
+
OBJECT_METHODS = Object.public_instance_methods.freeze
|
|
42
|
+
|
|
39
43
|
def initialize(response_builder, node_context, dispatcher, uri, resolver, rbi_index)
|
|
40
44
|
@response_builder = response_builder
|
|
41
45
|
@node_context = node_context
|
|
@@ -81,7 +85,10 @@ module RubyLsp
|
|
|
81
85
|
|
|
82
86
|
range = loc_to_range(node.message_loc || node.location)
|
|
83
87
|
|
|
84
|
-
@rbi_index.methods_for(type)
|
|
88
|
+
rbi_methods = @rbi_index.methods_for(type)
|
|
89
|
+
methods = rbi_methods.empty? ? core_ruby_methods_for(type) : rbi_methods
|
|
90
|
+
|
|
91
|
+
methods.each do |method_name, sig_or_def|
|
|
85
92
|
@response_builder << Interface::CompletionItem.new(
|
|
86
93
|
label: method_name,
|
|
87
94
|
filter_text: method_name,
|
|
@@ -188,6 +195,18 @@ module RubyLsp
|
|
|
188
195
|
end
|
|
189
196
|
end
|
|
190
197
|
|
|
198
|
+
# Returns method names for core Ruby types (String, Integer, Array, etc.)
|
|
199
|
+
# via reflection when the RBI index has no entries for the type.
|
|
200
|
+
# Excludes Object/Kernel/BasicObject methods to keep the list focused.
|
|
201
|
+
def core_ruby_methods_for(type)
|
|
202
|
+
klass = Object.const_get(type)
|
|
203
|
+
return {} unless klass.is_a?(Module)
|
|
204
|
+
(klass.public_instance_methods - OBJECT_METHODS).sort
|
|
205
|
+
.each_with_object({}) { |m, h| h[m.to_s] = nil }
|
|
206
|
+
rescue NameError, TypeError
|
|
207
|
+
{}
|
|
208
|
+
end
|
|
209
|
+
|
|
191
210
|
# Converts a Prism location (1-based lines) to an LSP Range (0-based).
|
|
192
211
|
def loc_to_range(loc)
|
|
193
212
|
Interface::Range.new(
|
|
@@ -83,7 +83,66 @@ module RubyLsp
|
|
|
83
83
|
|
|
84
84
|
def on_constant_read_node_enter(node)
|
|
85
85
|
return unless node.equal?(@node_context.node)
|
|
86
|
-
|
|
86
|
+
|
|
87
|
+
bare_name = node.name.to_s
|
|
88
|
+
nesting = @node_context.instance_variable_get(:@nesting_nodes)
|
|
89
|
+
|
|
90
|
+
# When the constant is the leading segment of a :: path, look up the full
|
|
91
|
+
# path first. This prevents a same-named ancestor namespace (e.g.
|
|
92
|
+
# Routes::Restrictions) from winning over the intended top-level one.
|
|
93
|
+
if (full_path = leading_constant_path(node))
|
|
94
|
+
lexical_candidates(full_path, nesting).each do |candidate|
|
|
95
|
+
file_loc = @rbi_index.const_source_for(candidate)
|
|
96
|
+
# Reject wrong RBI entries — Tapioca can point to the wrong file.
|
|
97
|
+
if file_loc && @doc_extractor
|
|
98
|
+
file_loc = nil unless @doc_extractor.exact_constant_line(file_loc[:file], candidate)
|
|
99
|
+
end
|
|
100
|
+
file_loc ||= @doc_extractor&.find_constant_source(candidate)
|
|
101
|
+
next unless file_loc
|
|
102
|
+
|
|
103
|
+
# Right file found via the full path. Navigate to where bare_name is
|
|
104
|
+
# defined within it (the enclosing module, not the child class).
|
|
105
|
+
line = @doc_extractor&.exact_constant_line(file_loc[:file], bare_name)
|
|
106
|
+
# Navigate even when bare_name has no separate declaration
|
|
107
|
+
# (e.g. `class Restrictions::CreditorRestrictor` with no `module Restrictions`).
|
|
108
|
+
# The RBI verification above already confirmed this is the right file.
|
|
109
|
+
push_location(file_loc[:file], line || 1)
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Successor-filter fallback: read the immediate child from the parent
|
|
115
|
+
# ConstantPathNode directly. This runs whether or not leading_constant_path
|
|
116
|
+
# succeeded — the parent is always available from @node_context. Candidate
|
|
117
|
+
# "Routes::Restrictions" is rejected when "Routes::Restrictions::CreditorRestrictor"
|
|
118
|
+
# doesn't exist; "Restrictions" is accepted because "Restrictions::CreditorRestrictor" does.
|
|
119
|
+
path_parent = @node_context.parent
|
|
120
|
+
if path_parent.is_a?(Prism::ConstantPathNode)
|
|
121
|
+
suffix = path_parent.child.slice.delete_prefix("::") rescue nil
|
|
122
|
+
if suffix && !suffix.empty?
|
|
123
|
+
lexical_candidates(bare_name, nesting).each do |candidate|
|
|
124
|
+
qualified_child = "#{candidate}::#{suffix}"
|
|
125
|
+
child_loc = @rbi_index.const_source_for(qualified_child)
|
|
126
|
+
if child_loc && @doc_extractor
|
|
127
|
+
child_loc = nil unless @doc_extractor.exact_constant_line(child_loc[:file], qualified_child)
|
|
128
|
+
end
|
|
129
|
+
child_loc ||= @doc_extractor&.find_constant_source(qualified_child)
|
|
130
|
+
next unless child_loc
|
|
131
|
+
|
|
132
|
+
location = @rbi_index.const_source_for(candidate)
|
|
133
|
+
if location && @doc_extractor
|
|
134
|
+
location = nil unless @doc_extractor.exact_constant_line(location[:file], candidate)
|
|
135
|
+
end
|
|
136
|
+
location ||= @doc_extractor&.find_constant_source(candidate)
|
|
137
|
+
next unless location
|
|
138
|
+
|
|
139
|
+
push_location(location[:file], location[:line])
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
goto_constant(bare_name)
|
|
87
146
|
end
|
|
88
147
|
|
|
89
148
|
def on_constant_path_node_enter(node)
|
|
@@ -293,6 +352,15 @@ module RubyLsp
|
|
|
293
352
|
|
|
294
353
|
lexical_candidates(bare_name, nesting).each do |candidate|
|
|
295
354
|
location = @rbi_index.const_source_for(candidate)
|
|
355
|
+
|
|
356
|
+
# Verify the RBI source comment actually defines this constant.
|
|
357
|
+
# Tapioca can write a wrong path (e.g. Routes::Restrictions when the
|
|
358
|
+
# real constant is top-level Restrictions). Reject the RBI result when
|
|
359
|
+
# the target file does not define the expected fully-qualified name.
|
|
360
|
+
if location && @doc_extractor
|
|
361
|
+
location = nil unless @doc_extractor.exact_constant_line(location[:file], candidate)
|
|
362
|
+
end
|
|
363
|
+
|
|
296
364
|
location ||= @doc_extractor&.find_constant_source(candidate)
|
|
297
365
|
next unless location
|
|
298
366
|
|
|
@@ -355,6 +423,25 @@ module RubyLsp
|
|
|
355
423
|
nil
|
|
356
424
|
end
|
|
357
425
|
|
|
426
|
+
# Returns the full constant path string when +node+ is the leading
|
|
427
|
+
# constant of a ConstantPathNode (e.g. returns "Restrictions::CreditorRestrictor"
|
|
428
|
+
# when +node+ is the `Restrictions` ConstantReadNode in that expression).
|
|
429
|
+
# Returns nil when the constant stands alone (not part of a :: path).
|
|
430
|
+
#
|
|
431
|
+
# ruby-lsp sets NodeContext#parent to the immediate AST parent of the cursor
|
|
432
|
+
# node. For `Restrictions` in `Restrictions::CreditorRestrictor`, that parent
|
|
433
|
+
# is the ConstantPathNode. We confirm our node is the left-hand side by
|
|
434
|
+
# checking that the path's own .parent field (Prism's LHS of ::) is our node.
|
|
435
|
+
def leading_constant_path(node)
|
|
436
|
+
path_node = @node_context.parent
|
|
437
|
+
return nil unless path_node.is_a?(Prism::ConstantPathNode)
|
|
438
|
+
return nil unless path_node.parent.equal?(node)
|
|
439
|
+
|
|
440
|
+
path_node.slice.delete_prefix("::")
|
|
441
|
+
rescue
|
|
442
|
+
nil
|
|
443
|
+
end
|
|
444
|
+
|
|
358
445
|
def push_location(file, line)
|
|
359
446
|
target_uri = "file://#{file}"
|
|
360
447
|
|
|
@@ -180,6 +180,12 @@ module RubyLsp
|
|
|
180
180
|
return unless @doc_extractor
|
|
181
181
|
|
|
182
182
|
doc = @doc_extractor.extract_for_constant(type)
|
|
183
|
+
|
|
184
|
+
if doc.nil?
|
|
185
|
+
loc = @rbi_index.const_source_for(type)
|
|
186
|
+
doc = @doc_extractor.extract_for_location(loc) if loc
|
|
187
|
+
end
|
|
188
|
+
|
|
183
189
|
return unless doc
|
|
184
190
|
|
|
185
191
|
@response_builder.push("**`#{type}`**\n\n#{doc}", category: :documentation)
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pink_spoon
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jānis Harbs
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
10
|
+
date: 2026-06-25 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: prism
|
|
@@ -40,7 +39,6 @@ dependencies:
|
|
|
40
39
|
version: '0.22'
|
|
41
40
|
description: Augments ruby-lsp hover and go-to-definition with type information from
|
|
42
41
|
sorbet/rbi/**/*.rbi. Drop it in your Gemfile and it just works.
|
|
43
|
-
email:
|
|
44
42
|
executables:
|
|
45
43
|
- pink-spoon
|
|
46
44
|
extensions: []
|
|
@@ -60,10 +58,8 @@ files:
|
|
|
60
58
|
- lib/ruby_lsp/pink_spoon/completion_listener.rb
|
|
61
59
|
- lib/ruby_lsp/pink_spoon/definition_listener.rb
|
|
62
60
|
- lib/ruby_lsp/pink_spoon/hover_listener.rb
|
|
63
|
-
homepage:
|
|
64
61
|
licenses: []
|
|
65
62
|
metadata: {}
|
|
66
|
-
post_install_message:
|
|
67
63
|
rdoc_options: []
|
|
68
64
|
require_paths:
|
|
69
65
|
- lib
|
|
@@ -78,8 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
78
74
|
- !ruby/object:Gem::Version
|
|
79
75
|
version: '0'
|
|
80
76
|
requirements: []
|
|
81
|
-
rubygems_version: 3.
|
|
82
|
-
signing_key:
|
|
77
|
+
rubygems_version: 3.6.2
|
|
83
78
|
specification_version: 4
|
|
84
79
|
summary: 'ruby-lsp addon: resolves types from Sorbet RBI files'
|
|
85
80
|
test_files: []
|