pink_spoon 0.1.1 → 0.1.2
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/rbi_index.rb +82 -17
- data/lib/pink_spoon/server.rb +95 -14
- data/lib/pink_spoon/version.rb +1 -1
- data/lib/ruby_lsp/pink_spoon/addon.rb +108 -1
- data/lib/ruby_lsp/pink_spoon/definition_listener.rb +58 -31
- data/lib/ruby_lsp/pink_spoon/hover_listener.rb +81 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7bf9b870581c1eb0fc630bb13d4ef6137767dd64d7546dcb74d92859a20e0f6
|
|
4
|
+
data.tar.gz: 7fce3bba4461680ad220526783e3179dd238fe536b83402fd945938fabf18453
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f4cff255afdd927f04e5c4d17116a83374b8f97490713b16d158b5bd025915c7c4ac190e9f59ba1f01f8ca1fb2264adc0cd513f464c822b27e02186b46975ca
|
|
7
|
+
data.tar.gz: 7d4ffa93a4cd9d458b53897bc5878499dc1356d045c3a3d18ba72dd44bc9a719118f7d9bd3ebce95e23a8c0f48a227883b9d5167829cb5623f7bf4abff89a97f
|
data/lib/pink_spoon/rbi_index.rb
CHANGED
|
@@ -14,14 +14,16 @@ module PinkSpoon
|
|
|
14
14
|
# though the method is defined on Hesiod::Gauge (which Hesiod extends).
|
|
15
15
|
class RbiIndex
|
|
16
16
|
def initialize(root_path)
|
|
17
|
-
@root_path
|
|
18
|
-
@sigs
|
|
19
|
-
@types
|
|
20
|
-
@defs
|
|
21
|
-
@sources
|
|
22
|
-
@locations
|
|
23
|
-
@mixins
|
|
24
|
-
@params
|
|
17
|
+
@root_path = root_path
|
|
18
|
+
@sigs = Hash.new { |h, k| h[k] = {} }
|
|
19
|
+
@types = Hash.new { |h, k| h[k] = {} }
|
|
20
|
+
@defs = Hash.new { |h, k| h[k] = {} }
|
|
21
|
+
@sources = Hash.new { |h, k| h[k] = {} }
|
|
22
|
+
@locations = Hash.new { |h, k| h[k] = {} }
|
|
23
|
+
@mixins = Hash.new { |h, k| h[k] = [] }
|
|
24
|
+
@params = Hash.new { |h, k| h[k] = {} }
|
|
25
|
+
@const_locations = {}
|
|
26
|
+
@const_resolve_cache = {}
|
|
25
27
|
build
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -109,6 +111,20 @@ module PinkSpoon
|
|
|
109
111
|
@mixins[normalise(type)].dup
|
|
110
112
|
end
|
|
111
113
|
|
|
114
|
+
# Returns { file:, line: } pointing at the real gem source for a constant,
|
|
115
|
+
# resolved from the "# source://" comment in the RBI file, or nil.
|
|
116
|
+
# Resolution is deferred to call time and nil results are not cached so
|
|
117
|
+
# transient Bundler state changes don't permanently break lookups.
|
|
118
|
+
def const_source_for(type)
|
|
119
|
+
raw = @const_locations[normalise(type)]
|
|
120
|
+
return nil unless raw
|
|
121
|
+
cached = @const_resolve_cache[raw]
|
|
122
|
+
return cached if cached
|
|
123
|
+
loc = resolve_source_uri(raw)
|
|
124
|
+
@const_resolve_cache[raw] = loc if loc
|
|
125
|
+
loc
|
|
126
|
+
end
|
|
127
|
+
|
|
112
128
|
# Returns { param_name => type_string } for type#method, or nil.
|
|
113
129
|
# Follows the mixin chain.
|
|
114
130
|
def params_for(type, method_name, _seen = [])
|
|
@@ -186,6 +202,44 @@ module PinkSpoon
|
|
|
186
202
|
visitor.mixins.each do |type, mixin_list|
|
|
187
203
|
@mixins[normalise(type)].concat(mixin_list.map { normalise(_1) }).uniq!
|
|
188
204
|
end
|
|
205
|
+
|
|
206
|
+
visitor.const_sources.each do |type, raw_uri|
|
|
207
|
+
@const_locations[normalise(type)] = raw_uri
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Parses "# source://gem-name//lib/path/to/file.rb#42" and resolves
|
|
212
|
+
# the gem name to a real file path.
|
|
213
|
+
#
|
|
214
|
+
# Primary: scan $LOAD_PATH for entries containing /gems/<gem-name>-<version>/.
|
|
215
|
+
# $LOAD_PATH is fixed at process start and unaffected by Bundler state changes
|
|
216
|
+
# that ruby-lsp triggers during its indexing phase.
|
|
217
|
+
#
|
|
218
|
+
# Fallback: scan Gem.path directories (covers cases where the gem's lib dir
|
|
219
|
+
# is not explicitly on the load path).
|
|
220
|
+
def resolve_source_uri(comment)
|
|
221
|
+
m = comment.match(%r{source://([^/]+)//(.+?)#(\d+)})
|
|
222
|
+
return nil unless m
|
|
223
|
+
gem_name, rel_path, line = m[1], m[2], m[3].to_i
|
|
224
|
+
lp_pattern = %r{/gems/(#{Regexp.escape(gem_name)}-[^/]+)/}
|
|
225
|
+
|
|
226
|
+
$LOAD_PATH.each do |lp|
|
|
227
|
+
next unless (lm = lp.match(lp_pattern))
|
|
228
|
+
gem_dir = lp[0, lp.index(lm[0]) + lm[0].length - 1]
|
|
229
|
+
file = File.join(gem_dir, rel_path)
|
|
230
|
+
return { file: file, line: line } if File.exist?(file)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
Gem.path.each do |gem_base|
|
|
234
|
+
Dir.glob("#{gem_base}/gems/#{gem_name}-*/").sort.reverse.each do |gem_dir|
|
|
235
|
+
file = File.join(gem_dir, rel_path)
|
|
236
|
+
return { file: file, line: line } if File.exist?(file)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
nil
|
|
241
|
+
rescue
|
|
242
|
+
nil
|
|
189
243
|
end
|
|
190
244
|
|
|
191
245
|
def normalise(type)
|
|
@@ -198,25 +252,31 @@ module PinkSpoon
|
|
|
198
252
|
# - extend/include calls (mixins)
|
|
199
253
|
# ------------------------------------------------------------------
|
|
200
254
|
class RbiVisitor < Prism::Visitor
|
|
201
|
-
attr_reader :entries, :mixins
|
|
255
|
+
attr_reader :entries, :mixins, :const_sources
|
|
202
256
|
|
|
203
257
|
def initialize(comment_map = {})
|
|
204
|
-
@entries
|
|
205
|
-
@mixins
|
|
206
|
-
@
|
|
207
|
-
@
|
|
208
|
-
@
|
|
258
|
+
@entries = []
|
|
259
|
+
@mixins = Hash.new { |h, k| h[k] = [] }
|
|
260
|
+
@const_sources = {}
|
|
261
|
+
@scope = []
|
|
262
|
+
@pending_sig = nil
|
|
263
|
+
@comment_map = comment_map
|
|
209
264
|
end
|
|
210
265
|
|
|
211
266
|
def visit_module_node(node)
|
|
212
|
-
|
|
267
|
+
source_comment = source_comment_at(node.location.start_line)
|
|
268
|
+
push_scope(node.constant_path) do
|
|
269
|
+
@const_sources[@scope.join("::")] = source_comment if source_comment
|
|
270
|
+
super
|
|
271
|
+
end
|
|
213
272
|
end
|
|
214
273
|
|
|
215
274
|
def visit_class_node(node)
|
|
275
|
+
source_comment = source_comment_at(node.location.start_line)
|
|
216
276
|
push_scope(node.constant_path) do
|
|
217
|
-
|
|
277
|
+
type = @scope.join("::")
|
|
278
|
+
@const_sources[type] = source_comment if source_comment
|
|
218
279
|
if node.superclass
|
|
219
|
-
type = @scope.join("::")
|
|
220
280
|
parent = const_path_to_string(node.superclass).delete_prefix("::")
|
|
221
281
|
@mixins[type] << parent unless @mixins[type].include?(parent)
|
|
222
282
|
end
|
|
@@ -263,6 +323,11 @@ module PinkSpoon
|
|
|
263
323
|
|
|
264
324
|
private
|
|
265
325
|
|
|
326
|
+
def source_comment_at(start_line)
|
|
327
|
+
c = @comment_map[start_line - 1]
|
|
328
|
+
c if c&.include?("source://")
|
|
329
|
+
end
|
|
330
|
+
|
|
266
331
|
def record_mixins(call_node)
|
|
267
332
|
type = @scope.join("::")
|
|
268
333
|
return if type.empty?
|
data/lib/pink_spoon/server.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "prism"
|
|
4
5
|
require_relative "rbi_index"
|
|
5
6
|
require_relative "constant_resolver"
|
|
6
7
|
require_relative "definition_finder"
|
|
@@ -56,6 +57,8 @@ module PinkSpoon
|
|
|
56
57
|
exit(0)
|
|
57
58
|
when "textDocument/definition"
|
|
58
59
|
write_response(id, on_definition(req[:params]))
|
|
60
|
+
when "textDocument/diagnostic"
|
|
61
|
+
write_response(id, { kind: "full", items: [] })
|
|
59
62
|
when "textDocument/hover"
|
|
60
63
|
write_response(id, on_hover(req[:params]))
|
|
61
64
|
else
|
|
@@ -92,23 +95,19 @@ module PinkSpoon
|
|
|
92
95
|
def on_definition(params)
|
|
93
96
|
return nil unless @rbi_index
|
|
94
97
|
|
|
95
|
-
file
|
|
96
|
-
line
|
|
97
|
-
col
|
|
98
|
+
file = uri_to_path(params.dig(:textDocument, :uri))
|
|
99
|
+
line = params.dig(:position, :line)
|
|
100
|
+
col = params.dig(:position, :character)
|
|
98
101
|
|
|
99
102
|
resolved = @constant_resolver.resolve_at(file, line, col)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
if resolved
|
|
104
|
+
location = @definition_finder.find(resolved[:type], resolved[:method])
|
|
105
|
+
return location_response(location[:file], location[:line]) if location
|
|
106
|
+
end
|
|
104
107
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
start: { line: location[:line] - 1, character: 0 },
|
|
109
|
-
end: { line: location[:line] - 1, character: 0 },
|
|
110
|
-
},
|
|
111
|
-
}
|
|
108
|
+
local_variable_definition(file, line, col)
|
|
109
|
+
rescue
|
|
110
|
+
nil
|
|
112
111
|
end
|
|
113
112
|
|
|
114
113
|
def on_hover(params)
|
|
@@ -132,6 +131,34 @@ module PinkSpoon
|
|
|
132
131
|
}
|
|
133
132
|
end
|
|
134
133
|
|
|
134
|
+
def local_variable_definition(file, lsp_line, col)
|
|
135
|
+
return nil unless file && File.exist?(file)
|
|
136
|
+
|
|
137
|
+
result = Prism.parse_file(file)
|
|
138
|
+
return nil unless result.success?
|
|
139
|
+
|
|
140
|
+
prism_line = lsp_line + 1
|
|
141
|
+
var_node = CursorLocalVarFinder.new(prism_line, col).tap { |f| f.visit(result.value) }.result
|
|
142
|
+
return nil unless var_node
|
|
143
|
+
|
|
144
|
+
decl_line = LocalVarDeclarationFinder.new(var_node.name.to_s, prism_line).tap { |f| f.visit(result.value) }.line
|
|
145
|
+
return nil unless decl_line
|
|
146
|
+
|
|
147
|
+
location_response(file, decl_line)
|
|
148
|
+
rescue
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def location_response(file, line)
|
|
153
|
+
{
|
|
154
|
+
uri: path_to_uri(file),
|
|
155
|
+
range: {
|
|
156
|
+
start: { line: line - 1, character: 0 },
|
|
157
|
+
end: { line: line - 1, character: 0 },
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
135
162
|
# ------------------------------------------------------------------
|
|
136
163
|
# Protocol helpers
|
|
137
164
|
# ------------------------------------------------------------------
|
|
@@ -170,4 +197,58 @@ module PinkSpoon
|
|
|
170
197
|
"file://#{path}"
|
|
171
198
|
end
|
|
172
199
|
end
|
|
200
|
+
|
|
201
|
+
# Finds the LocalVariableReadNode whose source range covers the given cursor.
|
|
202
|
+
class CursorLocalVarFinder < Prism::Visitor
|
|
203
|
+
attr_reader :result
|
|
204
|
+
|
|
205
|
+
def initialize(line, col)
|
|
206
|
+
@line = line
|
|
207
|
+
@col = col
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def visit_local_variable_read_node(node)
|
|
211
|
+
loc = node.location
|
|
212
|
+
if loc.start_line == @line &&
|
|
213
|
+
loc.start_column <= @col &&
|
|
214
|
+
loc.end_column > @col
|
|
215
|
+
@result = node
|
|
216
|
+
end
|
|
217
|
+
super
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Finds the line where a local variable was last declared (parameter or
|
|
222
|
+
# assignment) before a given line number.
|
|
223
|
+
class LocalVarDeclarationFinder < Prism::Visitor
|
|
224
|
+
def initialize(var_name, before_line)
|
|
225
|
+
@var_name = var_name
|
|
226
|
+
@before_line = before_line
|
|
227
|
+
@candidates = []
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def line
|
|
231
|
+
@candidates.last
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def visit_local_variable_write_node(node) = record(node) || super
|
|
235
|
+
def visit_local_variable_operator_write_node(node) = record(node) || super
|
|
236
|
+
def visit_local_variable_or_write_node(node) = record(node) || super
|
|
237
|
+
def visit_local_variable_and_write_node(node) = record(node) || super
|
|
238
|
+
def visit_required_parameter_node(node) = record(node) || super
|
|
239
|
+
def visit_optional_parameter_node(node) = record(node) || super
|
|
240
|
+
def visit_rest_parameter_node(node) = record(node) || super
|
|
241
|
+
def visit_keyword_rest_parameter_node(node) = record(node) || super
|
|
242
|
+
def visit_block_parameter_node(node) = record(node) || super
|
|
243
|
+
def visit_required_keyword_parameter_node(node) = record(node) || super
|
|
244
|
+
def visit_optional_keyword_parameter_node(node) = record(node) || super
|
|
245
|
+
|
|
246
|
+
private
|
|
247
|
+
|
|
248
|
+
def record(node)
|
|
249
|
+
return unless node.name.to_s == @var_name
|
|
250
|
+
line_no = node.location.start_line
|
|
251
|
+
@candidates << line_no if line_no <= @before_line
|
|
252
|
+
end
|
|
253
|
+
end
|
|
173
254
|
end
|
data/lib/pink_spoon/version.rb
CHANGED
|
@@ -13,6 +13,100 @@ require_relative "code_lens_listener"
|
|
|
13
13
|
|
|
14
14
|
module RubyLsp
|
|
15
15
|
module PinkSpoon
|
|
16
|
+
# Prepended to RubyLsp::Requests::Definition to handle local variable
|
|
17
|
+
# go-to-definition. Ruby-lsp excludes LocalVariableReadNode from its
|
|
18
|
+
# node_types list, so when the cursor is on a local variable (including
|
|
19
|
+
# when it is the receiver of a method call), ruby-lsp sets target = nil
|
|
20
|
+
# and calls no listeners. This patch does a second locate pass for that
|
|
21
|
+
# exact case and dispatches to pink-spoon's DefinitionListener.
|
|
22
|
+
module LocalVariableDefinitionFallback
|
|
23
|
+
class << self
|
|
24
|
+
attr_accessor :addon_instance
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(document, global_state, position, dispatcher, sorbet_level)
|
|
28
|
+
super
|
|
29
|
+
|
|
30
|
+
return if @target
|
|
31
|
+
return unless document.is_a?(RubyLsp::RubyDocument)
|
|
32
|
+
|
|
33
|
+
char_position, _ = document.find_index_by_position(position)
|
|
34
|
+
lv_context = RubyLsp::RubyDocument.locate(
|
|
35
|
+
document.ast,
|
|
36
|
+
char_position,
|
|
37
|
+
node_types: [Prism::LocalVariableReadNode],
|
|
38
|
+
code_units_cache: document.code_units_cache,
|
|
39
|
+
)
|
|
40
|
+
return unless lv_context.node.is_a?(Prism::LocalVariableReadNode)
|
|
41
|
+
|
|
42
|
+
addon = LocalVariableDefinitionFallback.addon_instance
|
|
43
|
+
return unless addon
|
|
44
|
+
|
|
45
|
+
@_lv_dispatcher = Prism::Dispatcher.new
|
|
46
|
+
addon.create_definition_listener(@response_builder, document.uri, lv_context, @_lv_dispatcher)
|
|
47
|
+
@_lv_target = lv_context.node
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def perform
|
|
51
|
+
result = super
|
|
52
|
+
if @_lv_target && @_lv_dispatcher && result.empty?
|
|
53
|
+
@_lv_dispatcher.dispatch_once(@_lv_target)
|
|
54
|
+
return @response_builder.response
|
|
55
|
+
end
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Same pattern as LocalVariableDefinitionFallback but for hover.
|
|
61
|
+
# Hover#perform returns nil when target is nil; here we intercept,
|
|
62
|
+
# do a second locate for LocalVariableReadNode, and return hover content.
|
|
63
|
+
module LocalVariableHoverFallback
|
|
64
|
+
class << self
|
|
65
|
+
attr_accessor :addon_instance
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize(document, global_state, position, dispatcher, sorbet_level)
|
|
69
|
+
super
|
|
70
|
+
|
|
71
|
+
return if @target
|
|
72
|
+
return unless document.is_a?(RubyLsp::RubyDocument)
|
|
73
|
+
|
|
74
|
+
char_position, _ = document.find_index_by_position(position)
|
|
75
|
+
lv_context = RubyLsp::RubyDocument.locate(
|
|
76
|
+
document.ast,
|
|
77
|
+
char_position,
|
|
78
|
+
node_types: [Prism::LocalVariableReadNode],
|
|
79
|
+
code_units_cache: document.code_units_cache,
|
|
80
|
+
)
|
|
81
|
+
return unless lv_context.node.is_a?(Prism::LocalVariableReadNode)
|
|
82
|
+
|
|
83
|
+
addon = LocalVariableHoverFallback.addon_instance
|
|
84
|
+
return unless addon
|
|
85
|
+
|
|
86
|
+
@_lv_hover_builder = RubyLsp::ResponseBuilders::Hover.new
|
|
87
|
+
@_lv_hover_dispatcher = Prism::Dispatcher.new
|
|
88
|
+
addon.create_hover_listener(@_lv_hover_builder, lv_context, @_lv_hover_dispatcher)
|
|
89
|
+
@_lv_hover_target = lv_context.node
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def perform
|
|
93
|
+
result = super
|
|
94
|
+
return result if result
|
|
95
|
+
|
|
96
|
+
return nil unless @_lv_hover_target && @_lv_hover_dispatcher
|
|
97
|
+
|
|
98
|
+
@_lv_hover_dispatcher.dispatch_once(@_lv_hover_target)
|
|
99
|
+
return nil if @_lv_hover_builder.empty?
|
|
100
|
+
|
|
101
|
+
RubyLsp::Interface::Hover.new(
|
|
102
|
+
contents: RubyLsp::Interface::MarkupContent.new(
|
|
103
|
+
kind: "markdown",
|
|
104
|
+
value: @_lv_hover_builder.response,
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
16
110
|
class Addon < ::RubyLsp::Addon
|
|
17
111
|
def activate(global_state, outgoing_queue)
|
|
18
112
|
root = global_state.workspace_path
|
|
@@ -20,9 +114,22 @@ module RubyLsp
|
|
|
20
114
|
@rbi_index = ::PinkSpoon::RbiIndex.new(root)
|
|
21
115
|
@constant_resolver = ::PinkSpoon::ConstantResolver.new(root, @rbi_index)
|
|
22
116
|
@doc_extractor = ::PinkSpoon::DocExtractor.new(root)
|
|
117
|
+
|
|
118
|
+
unless RubyLsp::Requests::Definition.ancestors.include?(LocalVariableDefinitionFallback)
|
|
119
|
+
LocalVariableDefinitionFallback.addon_instance = self
|
|
120
|
+
RubyLsp::Requests::Definition.prepend(LocalVariableDefinitionFallback)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
unless RubyLsp::Requests::Hover.ancestors.include?(LocalVariableHoverFallback)
|
|
124
|
+
LocalVariableHoverFallback.addon_instance = self
|
|
125
|
+
RubyLsp::Requests::Hover.prepend(LocalVariableHoverFallback)
|
|
126
|
+
end
|
|
23
127
|
end
|
|
24
128
|
|
|
25
|
-
def deactivate
|
|
129
|
+
def deactivate
|
|
130
|
+
LocalVariableDefinitionFallback.addon_instance = nil
|
|
131
|
+
LocalVariableHoverFallback.addon_instance = nil
|
|
132
|
+
end
|
|
26
133
|
|
|
27
134
|
def name = "Pink Spoon"
|
|
28
135
|
def version = ::PinkSpoon::VERSION
|
|
@@ -41,11 +41,14 @@ module RubyLsp
|
|
|
41
41
|
source_location(resolved) || rbi_location(resolved)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
# Bare call (no explicit receiver):
|
|
44
|
+
# Bare call (no explicit receiver): current file first, then nesting.
|
|
45
|
+
# Checking the current file first avoids the project-wide scan in
|
|
46
|
+
# nesting_location picking up a same-named method in a different file
|
|
47
|
+
# (e.g. alphabetically earlier au/.../download.rb vs gb/.../download.rb).
|
|
45
48
|
if location.nil? && node.receiver.nil?
|
|
46
49
|
method_name = node.name.to_s
|
|
47
|
-
location =
|
|
48
|
-
|
|
50
|
+
location = current_file_location(method_name) ||
|
|
51
|
+
nesting_location(method_name, nesting)
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
# Fallback: ruby-lsp resolves LocalVariableReadNode receivers up to the
|
|
@@ -150,6 +153,27 @@ module RubyLsp
|
|
|
150
153
|
nil
|
|
151
154
|
end
|
|
152
155
|
|
|
156
|
+
# Handles go-to-definition for real local variables (entry = ..., |entry|, etc).
|
|
157
|
+
def on_local_variable_read_node_enter(node)
|
|
158
|
+
return unless node.equal?(@node_context.node)
|
|
159
|
+
|
|
160
|
+
file = @uri.to_s.delete_prefix("file://")
|
|
161
|
+
return unless file && File.exist?(file)
|
|
162
|
+
|
|
163
|
+
var_name = node.name.to_s
|
|
164
|
+
read_line = node.location.start_line
|
|
165
|
+
|
|
166
|
+
result = Prism.parse_file(file)
|
|
167
|
+
finder = LocalVariableDefinitionFinder.new(var_name, read_line)
|
|
168
|
+
finder.visit(result.value)
|
|
169
|
+
line = finder.line
|
|
170
|
+
return unless line
|
|
171
|
+
|
|
172
|
+
push_location(file, line)
|
|
173
|
+
rescue
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
153
177
|
private
|
|
154
178
|
|
|
155
179
|
def navigate_super(node)
|
|
@@ -209,7 +233,8 @@ module RubyLsp
|
|
|
209
233
|
end
|
|
210
234
|
|
|
211
235
|
def goto_constant(type)
|
|
212
|
-
location = @
|
|
236
|
+
location = @rbi_index.const_source_for(type)
|
|
237
|
+
location ||= @doc_extractor&.find_constant_source(type)
|
|
213
238
|
return unless location
|
|
214
239
|
push_location(location[:file], location[:line])
|
|
215
240
|
end
|
|
@@ -238,27 +263,6 @@ module RubyLsp
|
|
|
238
263
|
rbi_location(resolved) || source_location(resolved)
|
|
239
264
|
end
|
|
240
265
|
|
|
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
266
|
# Last resort: scan the file the user is currently editing.
|
|
263
267
|
# Matches def, let(:name), let!(:name), subject(:name), subject!(:name).
|
|
264
268
|
def current_file_location(method_name)
|
|
@@ -280,13 +284,26 @@ module RubyLsp
|
|
|
280
284
|
end
|
|
281
285
|
|
|
282
286
|
def push_location(file, line)
|
|
287
|
+
target_uri = "file://#{file}"
|
|
288
|
+
|
|
289
|
+
begin
|
|
290
|
+
existing = @response_builder.response
|
|
291
|
+
return if existing.any? do |item|
|
|
292
|
+
(item.respond_to?(:target_uri) ? item.target_uri : item.uri) == target_uri
|
|
293
|
+
end
|
|
294
|
+
rescue
|
|
295
|
+
# Can't inspect existing results — proceed with the push
|
|
296
|
+
end
|
|
297
|
+
|
|
283
298
|
line_lsp = line - 1
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
299
|
+
range = Interface::Range.new(
|
|
300
|
+
start: Interface::Position.new(line: line_lsp, character: 0),
|
|
301
|
+
end: Interface::Position.new(line: line_lsp, character: 0),
|
|
302
|
+
)
|
|
303
|
+
@response_builder << Interface::LocationLink.new(
|
|
304
|
+
target_uri: target_uri,
|
|
305
|
+
target_range: range,
|
|
306
|
+
target_selection_range: range,
|
|
290
307
|
)
|
|
291
308
|
end
|
|
292
309
|
|
|
@@ -336,6 +353,16 @@ module RubyLsp
|
|
|
336
353
|
super
|
|
337
354
|
end
|
|
338
355
|
|
|
356
|
+
def visit_rest_parameter_node(node)
|
|
357
|
+
record(node)
|
|
358
|
+
super
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def visit_keyword_rest_parameter_node(node)
|
|
362
|
+
record(node)
|
|
363
|
+
super
|
|
364
|
+
end
|
|
365
|
+
|
|
339
366
|
# Block parameters: do |entry|
|
|
340
367
|
def visit_block_parameter_node(node)
|
|
341
368
|
record(node)
|
|
@@ -23,6 +23,7 @@ module RubyLsp
|
|
|
23
23
|
:on_constant_read_node_enter,
|
|
24
24
|
:on_constant_path_node_enter,
|
|
25
25
|
:on_instance_variable_read_node_enter,
|
|
26
|
+
:on_local_variable_read_node_enter,
|
|
26
27
|
)
|
|
27
28
|
end
|
|
28
29
|
|
|
@@ -71,6 +72,27 @@ module RubyLsp
|
|
|
71
72
|
@response_builder.push(content, category: :documentation)
|
|
72
73
|
end
|
|
73
74
|
|
|
75
|
+
def on_local_variable_read_node_enter(node)
|
|
76
|
+
return unless node.equal?(@node_context.node)
|
|
77
|
+
|
|
78
|
+
nesting = @node_context.instance_variable_get(:@nesting_nodes)
|
|
79
|
+
program = nesting&.find { |n| n.is_a?(Prism::ProgramNode) }
|
|
80
|
+
return unless program
|
|
81
|
+
|
|
82
|
+
finder = LocalVarKindFinder.new(node.name.to_s, node.location.start_line)
|
|
83
|
+
finder.visit(program)
|
|
84
|
+
return unless finder.kind
|
|
85
|
+
|
|
86
|
+
label = case finder.kind
|
|
87
|
+
when :method_param then "parameter"
|
|
88
|
+
when :block_param then "block parameter"
|
|
89
|
+
else "local variable"
|
|
90
|
+
end
|
|
91
|
+
@response_builder.push("**`#{node.name}`** — *#{label}*", category: :documentation)
|
|
92
|
+
rescue
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
74
96
|
private
|
|
75
97
|
|
|
76
98
|
def serve_constant_hover(type)
|
|
@@ -90,5 +112,64 @@ module RubyLsp
|
|
|
90
112
|
"#{rbi}\n\n---\n\n#{doc}"
|
|
91
113
|
end
|
|
92
114
|
end
|
|
115
|
+
|
|
116
|
+
# Determines whether a local variable is a method parameter, block
|
|
117
|
+
# parameter, or a plain local variable assignment.
|
|
118
|
+
class LocalVarKindFinder < Prism::Visitor
|
|
119
|
+
def initialize(var_name, before_line)
|
|
120
|
+
@var_name = var_name
|
|
121
|
+
@before_line = before_line
|
|
122
|
+
@candidates = []
|
|
123
|
+
@context = []
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def kind
|
|
127
|
+
@candidates.last&.last
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def visit_def_node(node)
|
|
131
|
+
@context.push(:method)
|
|
132
|
+
super
|
|
133
|
+
@context.pop
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def visit_block_node(node)
|
|
137
|
+
@context.push(:block)
|
|
138
|
+
super
|
|
139
|
+
@context.pop
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def visit_lambda_node(node)
|
|
143
|
+
@context.push(:block)
|
|
144
|
+
super
|
|
145
|
+
@context.pop
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def visit_local_variable_write_node(node) = record(node, :local_var) || super
|
|
149
|
+
def visit_local_variable_operator_write_node(node) = record(node, :local_var) || super
|
|
150
|
+
def visit_local_variable_or_write_node(node) = record(node, :local_var) || super
|
|
151
|
+
def visit_local_variable_and_write_node(node) = record(node, :local_var) || super
|
|
152
|
+
|
|
153
|
+
def visit_required_parameter_node(node) = record_param(node) || super
|
|
154
|
+
def visit_optional_parameter_node(node) = record_param(node) || super
|
|
155
|
+
def visit_rest_parameter_node(node) = record_param(node) || super
|
|
156
|
+
def visit_keyword_rest_parameter_node(node) = record_param(node) || super
|
|
157
|
+
def visit_block_parameter_node(node) = record_param(node) || super
|
|
158
|
+
def visit_required_keyword_parameter_node(node) = record_param(node) || super
|
|
159
|
+
def visit_optional_keyword_parameter_node(node) = record_param(node) || super
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def record_param(node)
|
|
164
|
+
kind = @context.last == :block ? :block_param : :method_param
|
|
165
|
+
record(node, kind)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def record(node, kind)
|
|
169
|
+
return unless node.name.to_s == @var_name
|
|
170
|
+
line_no = node.location.start_line
|
|
171
|
+
@candidates << [line_no, kind] if line_no <= @before_line
|
|
172
|
+
end
|
|
173
|
+
end
|
|
93
174
|
end
|
|
94
175
|
end
|
metadata
CHANGED
|
@@ -1,13 +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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jānis Harbs
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-06-
|
|
10
|
+
date: 2026-06-03 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: prism
|