ruby-lsp 0.13.2 → 0.13.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -0
  3. data/VERSION +1 -1
  4. data/lib/ruby_lsp/check_docs.rb +3 -3
  5. data/lib/ruby_lsp/document.rb +12 -0
  6. data/lib/ruby_lsp/executor.rb +77 -266
  7. data/lib/ruby_lsp/listener.rb +1 -50
  8. data/lib/ruby_lsp/listeners/code_lens.rb +233 -0
  9. data/lib/ruby_lsp/listeners/completion.rb +275 -0
  10. data/lib/ruby_lsp/listeners/definition.rb +158 -0
  11. data/lib/ruby_lsp/listeners/document_highlight.rb +556 -0
  12. data/lib/ruby_lsp/listeners/document_link.rb +162 -0
  13. data/lib/ruby_lsp/listeners/document_symbol.rb +223 -0
  14. data/lib/ruby_lsp/listeners/folding_ranges.rb +271 -0
  15. data/lib/ruby_lsp/listeners/hover.rb +152 -0
  16. data/lib/ruby_lsp/listeners/inlay_hints.rb +80 -0
  17. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +430 -0
  18. data/lib/ruby_lsp/listeners/signature_help.rb +74 -0
  19. data/lib/ruby_lsp/requests/code_action_resolve.rb +4 -4
  20. data/lib/ruby_lsp/requests/code_actions.rb +13 -4
  21. data/lib/ruby_lsp/requests/code_lens.rb +21 -221
  22. data/lib/ruby_lsp/requests/completion.rb +64 -244
  23. data/lib/ruby_lsp/requests/definition.rb +34 -147
  24. data/lib/ruby_lsp/requests/diagnostics.rb +17 -5
  25. data/lib/ruby_lsp/requests/document_highlight.rb +12 -536
  26. data/lib/ruby_lsp/requests/document_link.rb +11 -132
  27. data/lib/ruby_lsp/requests/document_symbol.rb +23 -210
  28. data/lib/ruby_lsp/requests/folding_ranges.rb +16 -252
  29. data/lib/ruby_lsp/requests/formatting.rb +4 -4
  30. data/lib/ruby_lsp/requests/hover.rb +48 -92
  31. data/lib/ruby_lsp/requests/inlay_hints.rb +23 -56
  32. data/lib/ruby_lsp/requests/on_type_formatting.rb +16 -4
  33. data/lib/ruby_lsp/requests/request.rb +17 -0
  34. data/lib/ruby_lsp/requests/selection_ranges.rb +4 -3
  35. data/lib/ruby_lsp/requests/semantic_highlighting.rb +21 -408
  36. data/lib/ruby_lsp/requests/show_syntax_tree.rb +4 -4
  37. data/lib/ruby_lsp/requests/signature_help.rb +43 -51
  38. data/lib/ruby_lsp/requests/support/common.rb +3 -2
  39. data/lib/ruby_lsp/requests/support/dependency_detector.rb +2 -0
  40. data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +2 -2
  41. data/lib/ruby_lsp/requests/workspace_symbol.rb +5 -4
  42. data/lib/ruby_lsp/requests.rb +1 -1
  43. data/lib/ruby_lsp/utils.rb +8 -0
  44. metadata +15 -4
  45. 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