ruby-lsp 0.13.2 → 0.13.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/README.md +30 -0
- data/VERSION +1 -1
- data/lib/ruby_lsp/check_docs.rb +3 -3
- data/lib/ruby_lsp/document.rb +12 -0
- data/lib/ruby_lsp/executor.rb +77 -266
- data/lib/ruby_lsp/listener.rb +1 -50
- data/lib/ruby_lsp/listeners/code_lens.rb +233 -0
- data/lib/ruby_lsp/listeners/completion.rb +275 -0
- data/lib/ruby_lsp/listeners/definition.rb +158 -0
- data/lib/ruby_lsp/listeners/document_highlight.rb +556 -0
- data/lib/ruby_lsp/listeners/document_link.rb +162 -0
- data/lib/ruby_lsp/listeners/document_symbol.rb +223 -0
- data/lib/ruby_lsp/listeners/folding_ranges.rb +271 -0
- data/lib/ruby_lsp/listeners/hover.rb +152 -0
- data/lib/ruby_lsp/listeners/inlay_hints.rb +80 -0
- data/lib/ruby_lsp/listeners/semantic_highlighting.rb +430 -0
- data/lib/ruby_lsp/listeners/signature_help.rb +74 -0
- data/lib/ruby_lsp/requests/code_action_resolve.rb +4 -4
- data/lib/ruby_lsp/requests/code_actions.rb +13 -4
- data/lib/ruby_lsp/requests/code_lens.rb +21 -221
- data/lib/ruby_lsp/requests/completion.rb +64 -244
- data/lib/ruby_lsp/requests/definition.rb +34 -147
- data/lib/ruby_lsp/requests/diagnostics.rb +17 -5
- data/lib/ruby_lsp/requests/document_highlight.rb +12 -536
- data/lib/ruby_lsp/requests/document_link.rb +11 -132
- data/lib/ruby_lsp/requests/document_symbol.rb +23 -210
- data/lib/ruby_lsp/requests/folding_ranges.rb +16 -252
- data/lib/ruby_lsp/requests/formatting.rb +4 -4
- data/lib/ruby_lsp/requests/hover.rb +48 -92
- data/lib/ruby_lsp/requests/inlay_hints.rb +23 -56
- data/lib/ruby_lsp/requests/on_type_formatting.rb +16 -4
- data/lib/ruby_lsp/requests/request.rb +17 -0
- data/lib/ruby_lsp/requests/selection_ranges.rb +4 -3
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +21 -408
- data/lib/ruby_lsp/requests/show_syntax_tree.rb +4 -4
- data/lib/ruby_lsp/requests/signature_help.rb +43 -51
- data/lib/ruby_lsp/requests/support/common.rb +3 -2
- data/lib/ruby_lsp/requests/support/dependency_detector.rb +2 -0
- data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +2 -2
- data/lib/ruby_lsp/requests/workspace_symbol.rb +5 -4
- data/lib/ruby_lsp/requests.rb +1 -1
- data/lib/ruby_lsp/utils.rb +8 -0
- metadata +17 -6
- data/lib/ruby_lsp/requests/base_request.rb +0 -24
@@ -0,0 +1,233 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "shellwords"
|
5
|
+
require_relative "../listener"
|
6
|
+
|
7
|
+
module RubyLsp
|
8
|
+
module Listeners
|
9
|
+
class CodeLens < Listener
|
10
|
+
extend T::Sig
|
11
|
+
extend T::Generic
|
12
|
+
|
13
|
+
BASE_COMMAND = T.let(
|
14
|
+
begin
|
15
|
+
Bundler.with_original_env { Bundler.default_lockfile }
|
16
|
+
"bundle exec ruby"
|
17
|
+
rescue Bundler::GemfileNotFound
|
18
|
+
"ruby"
|
19
|
+
end + " -Itest ",
|
20
|
+
String,
|
21
|
+
)
|
22
|
+
ACCESS_MODIFIERS = T.let([:public, :private, :protected], T::Array[Symbol])
|
23
|
+
SUPPORTED_TEST_LIBRARIES = T.let(["minitest", "test-unit"], T::Array[String])
|
24
|
+
|
25
|
+
ResponseType = type_member { { fixed: T::Array[Interface::CodeLens] } }
|
26
|
+
|
27
|
+
sig { override.returns(ResponseType) }
|
28
|
+
attr_reader :_response
|
29
|
+
|
30
|
+
sig do
|
31
|
+
params(
|
32
|
+
uri: URI::Generic,
|
33
|
+
lenses_configuration: RequestConfig,
|
34
|
+
dispatcher: Prism::Dispatcher,
|
35
|
+
).void
|
36
|
+
end
|
37
|
+
def initialize(uri, lenses_configuration, dispatcher)
|
38
|
+
@uri = T.let(uri, URI::Generic)
|
39
|
+
@_response = T.let([], ResponseType)
|
40
|
+
@path = T.let(uri.to_standardized_path, T.nilable(String))
|
41
|
+
# visibility_stack is a stack of [current_visibility, previous_visibility]
|
42
|
+
@visibility_stack = T.let([[:public, :public]], T::Array[T::Array[T.nilable(Symbol)]])
|
43
|
+
@class_stack = T.let([], T::Array[String])
|
44
|
+
@group_id = T.let(1, Integer)
|
45
|
+
@group_id_stack = T.let([], T::Array[Integer])
|
46
|
+
@lenses_configuration = lenses_configuration
|
47
|
+
|
48
|
+
super(dispatcher)
|
49
|
+
|
50
|
+
dispatcher.register(
|
51
|
+
self,
|
52
|
+
:on_class_node_enter,
|
53
|
+
:on_class_node_leave,
|
54
|
+
:on_def_node_enter,
|
55
|
+
:on_call_node_enter,
|
56
|
+
:on_call_node_leave,
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
sig { params(node: Prism::ClassNode).void }
|
61
|
+
def on_class_node_enter(node)
|
62
|
+
@visibility_stack.push([:public, :public])
|
63
|
+
class_name = node.constant_path.slice
|
64
|
+
@class_stack.push(class_name)
|
65
|
+
|
66
|
+
if @path && class_name.end_with?("Test")
|
67
|
+
add_test_code_lens(
|
68
|
+
node,
|
69
|
+
name: class_name,
|
70
|
+
command: generate_test_command(class_name: class_name),
|
71
|
+
kind: :group,
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
@group_id_stack.push(@group_id)
|
76
|
+
@group_id += 1
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { params(node: Prism::ClassNode).void }
|
80
|
+
def on_class_node_leave(node)
|
81
|
+
@visibility_stack.pop
|
82
|
+
@class_stack.pop
|
83
|
+
@group_id_stack.pop
|
84
|
+
end
|
85
|
+
|
86
|
+
sig { params(node: Prism::DefNode).void }
|
87
|
+
def on_def_node_enter(node)
|
88
|
+
class_name = @class_stack.last
|
89
|
+
return unless class_name&.end_with?("Test")
|
90
|
+
|
91
|
+
visibility, _ = @visibility_stack.last
|
92
|
+
if visibility == :public
|
93
|
+
method_name = node.name.to_s
|
94
|
+
if @path && method_name.start_with?("test_")
|
95
|
+
add_test_code_lens(
|
96
|
+
node,
|
97
|
+
name: method_name,
|
98
|
+
command: generate_test_command(method_name: method_name, class_name: class_name),
|
99
|
+
kind: :example,
|
100
|
+
)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
sig { params(node: Prism::CallNode).void }
|
106
|
+
def on_call_node_enter(node)
|
107
|
+
name = node.name
|
108
|
+
arguments = node.arguments
|
109
|
+
|
110
|
+
# If we found `private` by itself or `private def foo`
|
111
|
+
if ACCESS_MODIFIERS.include?(name)
|
112
|
+
if arguments.nil?
|
113
|
+
@visibility_stack.pop
|
114
|
+
@visibility_stack.push([name, name])
|
115
|
+
elsif arguments.arguments.first.is_a?(Prism::DefNode)
|
116
|
+
visibility, _ = @visibility_stack.pop
|
117
|
+
@visibility_stack.push([name, visibility])
|
118
|
+
end
|
119
|
+
|
120
|
+
return
|
121
|
+
end
|
122
|
+
|
123
|
+
if @path&.include?(GEMFILE_NAME) && name == :gem && arguments
|
124
|
+
return unless @lenses_configuration.enabled?(:gemfileLinks)
|
125
|
+
|
126
|
+
first_argument = arguments.arguments.first
|
127
|
+
return unless first_argument.is_a?(Prism::StringNode)
|
128
|
+
|
129
|
+
remote = resolve_gem_remote(first_argument)
|
130
|
+
return unless remote
|
131
|
+
|
132
|
+
add_open_gem_remote_code_lens(node, remote)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
sig { params(node: Prism::CallNode).void }
|
137
|
+
def on_call_node_leave(node)
|
138
|
+
_, prev_visibility = @visibility_stack.pop
|
139
|
+
@visibility_stack.push([prev_visibility, prev_visibility])
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
sig { params(node: Prism::Node, name: String, command: String, kind: Symbol).void }
|
145
|
+
def add_test_code_lens(node, name:, command:, kind:)
|
146
|
+
# don't add code lenses if the test library is not supported or unknown
|
147
|
+
return unless SUPPORTED_TEST_LIBRARIES.include?(DependencyDetector.instance.detected_test_library) && @path
|
148
|
+
|
149
|
+
arguments = [
|
150
|
+
@path,
|
151
|
+
name,
|
152
|
+
command,
|
153
|
+
{
|
154
|
+
start_line: node.location.start_line - 1,
|
155
|
+
start_column: node.location.start_column,
|
156
|
+
end_line: node.location.end_line - 1,
|
157
|
+
end_column: node.location.end_column,
|
158
|
+
},
|
159
|
+
]
|
160
|
+
|
161
|
+
grouping_data = { group_id: @group_id_stack.last, kind: kind }
|
162
|
+
grouping_data[:id] = @group_id if kind == :group
|
163
|
+
|
164
|
+
@_response << create_code_lens(
|
165
|
+
node,
|
166
|
+
title: "Run",
|
167
|
+
command_name: "rubyLsp.runTest",
|
168
|
+
arguments: arguments,
|
169
|
+
data: { type: "test", **grouping_data },
|
170
|
+
)
|
171
|
+
|
172
|
+
@_response << create_code_lens(
|
173
|
+
node,
|
174
|
+
title: "Run In Terminal",
|
175
|
+
command_name: "rubyLsp.runTestInTerminal",
|
176
|
+
arguments: arguments,
|
177
|
+
data: { type: "test_in_terminal", **grouping_data },
|
178
|
+
)
|
179
|
+
|
180
|
+
@_response << create_code_lens(
|
181
|
+
node,
|
182
|
+
title: "Debug",
|
183
|
+
command_name: "rubyLsp.debugTest",
|
184
|
+
arguments: arguments,
|
185
|
+
data: { type: "debug", **grouping_data },
|
186
|
+
)
|
187
|
+
end
|
188
|
+
|
189
|
+
sig { params(gem_name: Prism::StringNode).returns(T.nilable(String)) }
|
190
|
+
def resolve_gem_remote(gem_name)
|
191
|
+
spec = Gem::Specification.stubs.find { |gem| gem.name == gem_name.content }&.to_spec
|
192
|
+
return if spec.nil?
|
193
|
+
|
194
|
+
[spec.homepage, spec.metadata["source_code_uri"]].compact.find do |page|
|
195
|
+
page.start_with?("https://github.com", "https://gitlab.com")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
sig { params(class_name: String, method_name: T.nilable(String)).returns(String) }
|
200
|
+
def generate_test_command(class_name:, method_name: nil)
|
201
|
+
command = BASE_COMMAND + T.must(@path)
|
202
|
+
|
203
|
+
case DependencyDetector.instance.detected_test_library
|
204
|
+
when "minitest"
|
205
|
+
command += if method_name
|
206
|
+
" --name " + "/#{Shellwords.escape(class_name + "#" + method_name)}/"
|
207
|
+
else
|
208
|
+
" --name " + "/#{Shellwords.escape(class_name)}/"
|
209
|
+
end
|
210
|
+
when "test-unit"
|
211
|
+
command += " --testcase " + "/#{Shellwords.escape(class_name)}/"
|
212
|
+
|
213
|
+
if method_name
|
214
|
+
command += " --name " + Shellwords.escape(method_name)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
command
|
219
|
+
end
|
220
|
+
|
221
|
+
sig { params(node: Prism::CallNode, remote: String).void }
|
222
|
+
def add_open_gem_remote_code_lens(node, remote)
|
223
|
+
@_response << create_code_lens(
|
224
|
+
node,
|
225
|
+
title: "Open remote",
|
226
|
+
command_name: "rubyLsp.openLink",
|
227
|
+
arguments: [remote],
|
228
|
+
data: { type: "link" },
|
229
|
+
)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,275 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
module Listeners
|
6
|
+
class Completion < Listener
|
7
|
+
extend T::Sig
|
8
|
+
extend T::Generic
|
9
|
+
|
10
|
+
ResponseType = type_member { { fixed: T::Array[Interface::CompletionItem] } }
|
11
|
+
|
12
|
+
sig { override.returns(ResponseType) }
|
13
|
+
attr_reader :_response
|
14
|
+
|
15
|
+
sig do
|
16
|
+
params(
|
17
|
+
index: RubyIndexer::Index,
|
18
|
+
nesting: T::Array[String],
|
19
|
+
typechecker_enabled: T::Boolean,
|
20
|
+
dispatcher: Prism::Dispatcher,
|
21
|
+
).void
|
22
|
+
end
|
23
|
+
def initialize(index, nesting, typechecker_enabled, dispatcher)
|
24
|
+
super(dispatcher)
|
25
|
+
@_response = T.let([], ResponseType)
|
26
|
+
@index = index
|
27
|
+
@nesting = nesting
|
28
|
+
@typechecker_enabled = typechecker_enabled
|
29
|
+
|
30
|
+
dispatcher.register(
|
31
|
+
self,
|
32
|
+
:on_string_node_enter,
|
33
|
+
:on_constant_path_node_enter,
|
34
|
+
:on_constant_read_node_enter,
|
35
|
+
:on_call_node_enter,
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
sig { params(node: Prism::StringNode).void }
|
40
|
+
def on_string_node_enter(node)
|
41
|
+
@index.search_require_paths(node.content).map!(&:require_path).sort!.each do |path|
|
42
|
+
@_response << build_completion(T.must(path), node)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Handle completion on regular constant references (e.g. `Bar`)
|
47
|
+
sig { params(node: Prism::ConstantReadNode).void }
|
48
|
+
def on_constant_read_node_enter(node)
|
49
|
+
return if DependencyDetector.instance.typechecker
|
50
|
+
|
51
|
+
name = node.slice
|
52
|
+
candidates = @index.prefix_search(name, @nesting)
|
53
|
+
candidates.each do |entries|
|
54
|
+
complete_name = T.must(entries.first).name
|
55
|
+
@_response << build_entry_completion(
|
56
|
+
complete_name,
|
57
|
+
name,
|
58
|
+
node,
|
59
|
+
entries,
|
60
|
+
top_level?(complete_name),
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Handle completion on namespaced constant references (e.g. `Foo::Bar`)
|
66
|
+
sig { params(node: Prism::ConstantPathNode).void }
|
67
|
+
def on_constant_path_node_enter(node)
|
68
|
+
return if DependencyDetector.instance.typechecker
|
69
|
+
|
70
|
+
name = node.slice
|
71
|
+
|
72
|
+
top_level_reference = if name.start_with?("::")
|
73
|
+
name = name.delete_prefix("::")
|
74
|
+
true
|
75
|
+
else
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
# If we're trying to provide completion for an aliased namespace, we need to first discover it's real name in
|
80
|
+
# order to find which possible constants match the desired search
|
81
|
+
*namespace, incomplete_name = name.split("::")
|
82
|
+
aliased_namespace = T.must(namespace).join("::")
|
83
|
+
namespace_entries = @index.resolve(aliased_namespace, @nesting)
|
84
|
+
return unless namespace_entries
|
85
|
+
|
86
|
+
real_namespace = @index.follow_aliased_namespace(T.must(namespace_entries.first).name)
|
87
|
+
|
88
|
+
candidates = @index.prefix_search("#{real_namespace}::#{incomplete_name}", top_level_reference ? [] : @nesting)
|
89
|
+
candidates.each do |entries|
|
90
|
+
# The only time we may have a private constant reference from outside of the namespace is if we're dealing
|
91
|
+
# with ConstantPath and the entry name doesn't start with the current nesting
|
92
|
+
first_entry = T.must(entries.first)
|
93
|
+
next if first_entry.visibility == :private && !first_entry.name.start_with?("#{@nesting}::")
|
94
|
+
|
95
|
+
constant_name = T.must(first_entry.name.split("::").last)
|
96
|
+
|
97
|
+
full_name = aliased_namespace.empty? ? constant_name : "#{aliased_namespace}::#{constant_name}"
|
98
|
+
|
99
|
+
@_response << build_entry_completion(
|
100
|
+
full_name,
|
101
|
+
name,
|
102
|
+
node,
|
103
|
+
entries,
|
104
|
+
top_level_reference || top_level?(T.must(entries.first).name),
|
105
|
+
)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
sig { params(node: Prism::CallNode).void }
|
110
|
+
def on_call_node_enter(node)
|
111
|
+
return if @typechecker_enabled
|
112
|
+
return unless self_receiver?(node)
|
113
|
+
|
114
|
+
name = node.message
|
115
|
+
return unless name
|
116
|
+
|
117
|
+
receiver_entries = @index[@nesting.join("::")]
|
118
|
+
return unless receiver_entries
|
119
|
+
|
120
|
+
receiver = T.must(receiver_entries.first)
|
121
|
+
|
122
|
+
@index.prefix_search(name).each do |entries|
|
123
|
+
entry = entries.find { |e| e.is_a?(RubyIndexer::Entry::Member) && e.owner&.name == receiver.name }
|
124
|
+
next unless entry
|
125
|
+
|
126
|
+
@_response << build_method_completion(T.cast(entry, RubyIndexer::Entry::Member), node)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
sig do
|
133
|
+
params(
|
134
|
+
entry: RubyIndexer::Entry::Member,
|
135
|
+
node: Prism::CallNode,
|
136
|
+
).returns(Interface::CompletionItem)
|
137
|
+
end
|
138
|
+
def build_method_completion(entry, node)
|
139
|
+
name = entry.name
|
140
|
+
|
141
|
+
Interface::CompletionItem.new(
|
142
|
+
label: name,
|
143
|
+
filter_text: name,
|
144
|
+
text_edit: Interface::TextEdit.new(range: range_from_node(node), new_text: name),
|
145
|
+
kind: Constant::CompletionItemKind::METHOD,
|
146
|
+
label_details: Interface::CompletionItemLabelDetails.new(
|
147
|
+
detail: "(#{entry.parameters.map(&:decorated_name).join(", ")})",
|
148
|
+
description: entry.file_name,
|
149
|
+
),
|
150
|
+
documentation: markdown_from_index_entries(name, entry),
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
sig { params(label: String, node: Prism::StringNode).returns(Interface::CompletionItem) }
|
155
|
+
def build_completion(label, node)
|
156
|
+
# We should use the content location as we only replace the content and not the delimiters of the string
|
157
|
+
loc = node.content_loc
|
158
|
+
|
159
|
+
Interface::CompletionItem.new(
|
160
|
+
label: label,
|
161
|
+
text_edit: Interface::TextEdit.new(
|
162
|
+
range: range_from_location(loc),
|
163
|
+
new_text: label,
|
164
|
+
),
|
165
|
+
kind: Constant::CompletionItemKind::FILE,
|
166
|
+
)
|
167
|
+
end
|
168
|
+
|
169
|
+
sig do
|
170
|
+
params(
|
171
|
+
real_name: String,
|
172
|
+
incomplete_name: String,
|
173
|
+
node: Prism::Node,
|
174
|
+
entries: T::Array[RubyIndexer::Entry],
|
175
|
+
top_level: T::Boolean,
|
176
|
+
).returns(Interface::CompletionItem)
|
177
|
+
end
|
178
|
+
def build_entry_completion(real_name, incomplete_name, node, entries, top_level)
|
179
|
+
first_entry = T.must(entries.first)
|
180
|
+
kind = case first_entry
|
181
|
+
when RubyIndexer::Entry::Class
|
182
|
+
Constant::CompletionItemKind::CLASS
|
183
|
+
when RubyIndexer::Entry::Module
|
184
|
+
Constant::CompletionItemKind::MODULE
|
185
|
+
when RubyIndexer::Entry::Constant
|
186
|
+
Constant::CompletionItemKind::CONSTANT
|
187
|
+
else
|
188
|
+
Constant::CompletionItemKind::REFERENCE
|
189
|
+
end
|
190
|
+
|
191
|
+
insertion_text = real_name.dup
|
192
|
+
filter_text = real_name.dup
|
193
|
+
|
194
|
+
# If we have two entries with the same name inside the current namespace and the user selects the top level
|
195
|
+
# option, we have to ensure it's prefixed with `::` or else we're completing the wrong constant. For example:
|
196
|
+
# If we have the index with ["Foo::Bar", "Bar"], and we're providing suggestions for `B` inside a `Foo` module,
|
197
|
+
# then selecting the `Foo::Bar` option needs to complete to `Bar` and selecting the top level `Bar` option needs
|
198
|
+
# to complete to `::Bar`.
|
199
|
+
if top_level
|
200
|
+
insertion_text.prepend("::")
|
201
|
+
filter_text.prepend("::")
|
202
|
+
end
|
203
|
+
|
204
|
+
# If the user is searching for a constant inside the current namespace, then we prefer completing the short name
|
205
|
+
# of that constant. E.g.:
|
206
|
+
#
|
207
|
+
# module Foo
|
208
|
+
# class Bar
|
209
|
+
# end
|
210
|
+
#
|
211
|
+
# Foo::B # --> completion inserts `Bar` instead of `Foo::Bar`
|
212
|
+
# end
|
213
|
+
@nesting.each do |namespace|
|
214
|
+
prefix = "#{namespace}::"
|
215
|
+
shortened_name = insertion_text.delete_prefix(prefix)
|
216
|
+
|
217
|
+
# If a different entry exists for the shortened name, then there's a conflict and we should not shorten it
|
218
|
+
conflict_name = "#{@nesting.join("::")}::#{shortened_name}"
|
219
|
+
break if real_name != conflict_name && @index[conflict_name]
|
220
|
+
|
221
|
+
insertion_text = shortened_name
|
222
|
+
|
223
|
+
# If the user is typing a fully qualified name `Foo::Bar::Baz`, then we should not use the short name (e.g.:
|
224
|
+
# `Baz`) as filtering. So we only shorten the filter text if the user is not including the namespaces in their
|
225
|
+
# typing
|
226
|
+
filter_text.delete_prefix!(prefix) unless incomplete_name.start_with?(prefix)
|
227
|
+
end
|
228
|
+
|
229
|
+
# When using a top level constant reference (e.g.: `::Bar`), the editor includes the `::` as part of the filter.
|
230
|
+
# For these top level references, we need to include the `::` as part of the filter text or else it won't match
|
231
|
+
# the right entries in the index
|
232
|
+
Interface::CompletionItem.new(
|
233
|
+
label: real_name,
|
234
|
+
filter_text: filter_text,
|
235
|
+
text_edit: Interface::TextEdit.new(
|
236
|
+
range: range_from_node(node),
|
237
|
+
new_text: insertion_text,
|
238
|
+
),
|
239
|
+
kind: kind,
|
240
|
+
label_details: Interface::CompletionItemLabelDetails.new(
|
241
|
+
description: entries.map(&:file_name).join(","),
|
242
|
+
),
|
243
|
+
documentation: markdown_from_index_entries(real_name, entries),
|
244
|
+
)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Check if there are any conflicting names for `entry_name`, which would require us to use a top level reference.
|
248
|
+
# For example:
|
249
|
+
#
|
250
|
+
# ```ruby
|
251
|
+
# class Bar; end
|
252
|
+
#
|
253
|
+
# module Foo
|
254
|
+
# class Bar; end
|
255
|
+
#
|
256
|
+
# # in this case, the completion for `Bar` conflicts with `Foo::Bar`, so we can't suggest `Bar` as the
|
257
|
+
# # completion, but instead need to suggest `::Bar`
|
258
|
+
# B
|
259
|
+
# end
|
260
|
+
# ```
|
261
|
+
sig { params(entry_name: String).returns(T::Boolean) }
|
262
|
+
def top_level?(entry_name)
|
263
|
+
@nesting.length.downto(0).each do |i|
|
264
|
+
prefix = T.must(@nesting[0...i]).join("::")
|
265
|
+
full_name = prefix.empty? ? entry_name : "#{prefix}::#{entry_name}"
|
266
|
+
next if full_name == entry_name
|
267
|
+
|
268
|
+
return true if @index[full_name]
|
269
|
+
end
|
270
|
+
|
271
|
+
false
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
module Listeners
|
6
|
+
class Definition < Listener
|
7
|
+
extend T::Sig
|
8
|
+
extend T::Generic
|
9
|
+
|
10
|
+
ResponseType = type_member { { fixed: T.nilable(T.any(T::Array[Interface::Location], Interface::Location)) } }
|
11
|
+
|
12
|
+
sig { override.returns(ResponseType) }
|
13
|
+
attr_reader :_response
|
14
|
+
|
15
|
+
sig do
|
16
|
+
params(
|
17
|
+
uri: URI::Generic,
|
18
|
+
nesting: T::Array[String],
|
19
|
+
index: RubyIndexer::Index,
|
20
|
+
dispatcher: Prism::Dispatcher,
|
21
|
+
typechecker_enabled: T::Boolean,
|
22
|
+
).void
|
23
|
+
end
|
24
|
+
def initialize(uri, nesting, index, dispatcher, typechecker_enabled)
|
25
|
+
@uri = uri
|
26
|
+
@nesting = nesting
|
27
|
+
@index = index
|
28
|
+
@typechecker_enabled = typechecker_enabled
|
29
|
+
@_response = T.let(nil, ResponseType)
|
30
|
+
|
31
|
+
super(dispatcher)
|
32
|
+
|
33
|
+
dispatcher.register(
|
34
|
+
self,
|
35
|
+
:on_call_node_enter,
|
36
|
+
:on_constant_read_node_enter,
|
37
|
+
:on_constant_path_node_enter,
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
sig { params(node: Prism::CallNode).void }
|
42
|
+
def on_call_node_enter(node)
|
43
|
+
message = node.name
|
44
|
+
|
45
|
+
if message == :require || message == :require_relative
|
46
|
+
handle_require_definition(node)
|
47
|
+
else
|
48
|
+
handle_method_definition(node)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { params(node: Prism::ConstantPathNode).void }
|
53
|
+
def on_constant_path_node_enter(node)
|
54
|
+
find_in_index(node.slice)
|
55
|
+
end
|
56
|
+
|
57
|
+
sig { params(node: Prism::ConstantReadNode).void }
|
58
|
+
def on_constant_read_node_enter(node)
|
59
|
+
find_in_index(node.slice)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
sig { params(node: Prism::CallNode).void }
|
65
|
+
def handle_method_definition(node)
|
66
|
+
return unless self_receiver?(node)
|
67
|
+
|
68
|
+
message = node.message
|
69
|
+
return unless message
|
70
|
+
|
71
|
+
target_method = @index.resolve_method(message, @nesting.join("::"))
|
72
|
+
return unless target_method
|
73
|
+
|
74
|
+
location = target_method.location
|
75
|
+
file_path = target_method.file_path
|
76
|
+
return if @typechecker_enabled && not_in_dependencies?(file_path)
|
77
|
+
|
78
|
+
@_response = Interface::Location.new(
|
79
|
+
uri: URI::Generic.from_path(path: file_path).to_s,
|
80
|
+
range: Interface::Range.new(
|
81
|
+
start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
|
82
|
+
end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
|
83
|
+
),
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
sig { params(node: Prism::CallNode).void }
|
88
|
+
def handle_require_definition(node)
|
89
|
+
message = node.name
|
90
|
+
arguments = node.arguments
|
91
|
+
return unless arguments
|
92
|
+
|
93
|
+
argument = arguments.arguments.first
|
94
|
+
return unless argument.is_a?(Prism::StringNode)
|
95
|
+
|
96
|
+
case message
|
97
|
+
when :require
|
98
|
+
entry = @index.search_require_paths(argument.content).find do |indexable_path|
|
99
|
+
indexable_path.require_path == argument.content
|
100
|
+
end
|
101
|
+
|
102
|
+
if entry
|
103
|
+
candidate = entry.full_path
|
104
|
+
|
105
|
+
@_response = Interface::Location.new(
|
106
|
+
uri: URI::Generic.from_path(path: candidate).to_s,
|
107
|
+
range: Interface::Range.new(
|
108
|
+
start: Interface::Position.new(line: 0, character: 0),
|
109
|
+
end: Interface::Position.new(line: 0, character: 0),
|
110
|
+
),
|
111
|
+
)
|
112
|
+
end
|
113
|
+
when :require_relative
|
114
|
+
required_file = "#{argument.content}.rb"
|
115
|
+
path = @uri.to_standardized_path
|
116
|
+
current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : Dir.pwd
|
117
|
+
candidate = File.expand_path(File.join(current_folder, required_file))
|
118
|
+
|
119
|
+
@_response = Interface::Location.new(
|
120
|
+
uri: URI::Generic.from_path(path: candidate).to_s,
|
121
|
+
range: Interface::Range.new(
|
122
|
+
start: Interface::Position.new(line: 0, character: 0),
|
123
|
+
end: Interface::Position.new(line: 0, character: 0),
|
124
|
+
),
|
125
|
+
)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
sig { params(value: String).void }
|
130
|
+
def find_in_index(value)
|
131
|
+
entries = @index.resolve(value, @nesting)
|
132
|
+
return unless entries
|
133
|
+
|
134
|
+
# We should only allow jumping to the definition of private constants if the constant is defined in the same
|
135
|
+
# namespace as the reference
|
136
|
+
first_entry = T.must(entries.first)
|
137
|
+
return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{value}"
|
138
|
+
|
139
|
+
@_response = entries.filter_map do |entry|
|
140
|
+
location = entry.location
|
141
|
+
# If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
|
142
|
+
# additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants
|
143
|
+
# in the project, even if the files are typed false
|
144
|
+
file_path = entry.file_path
|
145
|
+
next if @typechecker_enabled && not_in_dependencies?(file_path)
|
146
|
+
|
147
|
+
Interface::Location.new(
|
148
|
+
uri: URI::Generic.from_path(path: file_path).to_s,
|
149
|
+
range: Interface::Range.new(
|
150
|
+
start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
|
151
|
+
end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
|
152
|
+
),
|
153
|
+
)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|