ruby-lsp 0.13.3 → 0.14.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp-doctor +2 -0
  5. data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +4 -8
  6. data/lib/ruby_indexer/lib/ruby_indexer/collector.rb +5 -1
  7. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +4 -2
  8. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +10 -5
  9. data/lib/ruby_indexer/test/classes_and_modules_test.rb +9 -0
  10. data/lib/ruby_indexer/test/index_test.rb +54 -3
  11. data/lib/ruby_lsp/addon.rb +34 -17
  12. data/lib/ruby_lsp/check_docs.rb +8 -8
  13. data/lib/ruby_lsp/executor.rb +28 -10
  14. data/lib/ruby_lsp/internal.rb +8 -6
  15. data/lib/ruby_lsp/listeners/code_lens.rb +54 -55
  16. data/lib/ruby_lsp/listeners/completion.rb +22 -18
  17. data/lib/ruby_lsp/listeners/definition.rb +31 -29
  18. data/lib/ruby_lsp/listeners/document_highlight.rb +6 -11
  19. data/lib/ruby_lsp/listeners/document_link.rb +6 -12
  20. data/lib/ruby_lsp/listeners/document_symbol.rb +194 -55
  21. data/lib/ruby_lsp/listeners/folding_ranges.rb +19 -23
  22. data/lib/ruby_lsp/listeners/hover.rb +36 -34
  23. data/lib/ruby_lsp/listeners/inlay_hints.rb +7 -13
  24. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +54 -124
  25. data/lib/ruby_lsp/listeners/signature_help.rb +15 -14
  26. data/lib/ruby_lsp/requests/code_lens.rb +11 -19
  27. data/lib/ruby_lsp/requests/completion.rb +7 -9
  28. data/lib/ruby_lsp/requests/definition.rb +10 -22
  29. data/lib/ruby_lsp/requests/document_highlight.rb +7 -5
  30. data/lib/ruby_lsp/requests/document_link.rb +7 -6
  31. data/lib/ruby_lsp/requests/document_symbol.rb +5 -11
  32. data/lib/ruby_lsp/requests/folding_ranges.rb +11 -6
  33. data/lib/ruby_lsp/requests/hover.rb +18 -24
  34. data/lib/ruby_lsp/requests/inlay_hints.rb +7 -8
  35. data/lib/ruby_lsp/requests/on_type_formatting.rb +12 -2
  36. data/lib/ruby_lsp/requests/semantic_highlighting.rb +10 -8
  37. data/lib/ruby_lsp/requests/signature_help.rb +53 -18
  38. data/lib/ruby_lsp/requests/support/common.rb +38 -10
  39. data/lib/ruby_lsp/requests/support/dependency_detector.rb +5 -1
  40. data/lib/ruby_lsp/requests.rb +0 -1
  41. data/lib/ruby_lsp/response_builders/collection_response_builder.rb +29 -0
  42. data/lib/ruby_lsp/response_builders/document_symbol.rb +57 -0
  43. data/lib/ruby_lsp/response_builders/hover.rb +49 -0
  44. data/lib/ruby_lsp/response_builders/response_builder.rb +16 -0
  45. data/lib/ruby_lsp/response_builders/semantic_highlighting.rb +199 -0
  46. data/lib/ruby_lsp/response_builders/signature_help.rb +28 -0
  47. data/lib/ruby_lsp/response_builders.rb +13 -0
  48. data/lib/ruby_lsp/server.rb +3 -3
  49. data/lib/ruby_lsp/setup_bundler.rb +64 -23
  50. data/lib/ruby_lsp/store.rb +4 -4
  51. metadata +17 -11
  52. data/lib/ruby_lsp/listener.rb +0 -33
  53. data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +0 -73
@@ -2,13 +2,12 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "shellwords"
5
- require_relative "../listener"
6
5
 
7
6
  module RubyLsp
8
7
  module Listeners
9
- class CodeLens < Listener
8
+ class CodeLens
10
9
  extend T::Sig
11
- extend T::Generic
10
+ include Requests::Support::Common
12
11
 
13
12
  BASE_COMMAND = T.let(
14
13
  begin
@@ -21,31 +20,25 @@ module RubyLsp
21
20
  )
22
21
  ACCESS_MODIFIERS = T.let([:public, :private, :protected], T::Array[Symbol])
23
22
  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
23
+ DESCRIBE_KEYWORD = T.let(:describe, Symbol)
24
+ IT_KEYWORD = T.let(:it, Symbol)
29
25
 
30
26
  sig do
31
27
  params(
28
+ response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
32
29
  uri: URI::Generic,
33
- lenses_configuration: RequestConfig,
34
30
  dispatcher: Prism::Dispatcher,
35
31
  ).void
36
32
  end
37
- def initialize(uri, lenses_configuration, dispatcher)
33
+ def initialize(response_builder, uri, dispatcher)
34
+ @response_builder = response_builder
38
35
  @uri = T.let(uri, URI::Generic)
39
- @_response = T.let([], ResponseType)
40
36
  @path = T.let(uri.to_standardized_path, T.nilable(String))
41
37
  # visibility_stack is a stack of [current_visibility, previous_visibility]
42
38
  @visibility_stack = T.let([[:public, :public]], T::Array[T::Array[T.nilable(Symbol)]])
43
- @class_stack = T.let([], T::Array[String])
39
+ @group_stack = T.let([], T::Array[String])
44
40
  @group_id = T.let(1, Integer)
45
41
  @group_id_stack = T.let([], T::Array[Integer])
46
- @lenses_configuration = lenses_configuration
47
-
48
- super(dispatcher)
49
42
 
50
43
  dispatcher.register(
51
44
  self,
@@ -61,13 +54,13 @@ module RubyLsp
61
54
  def on_class_node_enter(node)
62
55
  @visibility_stack.push([:public, :public])
63
56
  class_name = node.constant_path.slice
64
- @class_stack.push(class_name)
57
+ @group_stack.push(class_name)
65
58
 
66
59
  if @path && class_name.end_with?("Test")
67
60
  add_test_code_lens(
68
61
  node,
69
62
  name: class_name,
70
- command: generate_test_command(class_name: class_name),
63
+ command: generate_test_command(group_name: class_name),
71
64
  kind: :group,
72
65
  )
73
66
  end
@@ -79,13 +72,13 @@ module RubyLsp
79
72
  sig { params(node: Prism::ClassNode).void }
80
73
  def on_class_node_leave(node)
81
74
  @visibility_stack.pop
82
- @class_stack.pop
75
+ @group_stack.pop
83
76
  @group_id_stack.pop
84
77
  end
85
78
 
86
79
  sig { params(node: Prism::DefNode).void }
87
80
  def on_def_node_enter(node)
88
- class_name = @class_stack.last
81
+ class_name = @group_stack.last
89
82
  return unless class_name&.end_with?("Test")
90
83
 
91
84
  visibility, _ = @visibility_stack.last
@@ -95,7 +88,7 @@ module RubyLsp
95
88
  add_test_code_lens(
96
89
  node,
97
90
  name: method_name,
98
- command: generate_test_command(method_name: method_name, class_name: class_name),
91
+ command: generate_test_command(method_name: method_name, group_name: class_name),
99
92
  kind: :example,
100
93
  )
101
94
  end
@@ -120,16 +113,15 @@ module RubyLsp
120
113
  return
121
114
  end
122
115
 
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)
116
+ if [DESCRIBE_KEYWORD, IT_KEYWORD].include?(name)
117
+ case name
118
+ when DESCRIBE_KEYWORD
119
+ add_spec_code_lens(node, kind: :group)
120
+ @group_id_stack.push(@group_id)
121
+ @group_id += 1
122
+ when IT_KEYWORD
123
+ add_spec_code_lens(node, kind: :example)
124
+ end
133
125
  end
134
126
  end
135
127
 
@@ -137,6 +129,9 @@ module RubyLsp
137
129
  def on_call_node_leave(node)
138
130
  _, prev_visibility = @visibility_stack.pop
139
131
  @visibility_stack.push([prev_visibility, prev_visibility])
132
+ if node.name == DESCRIBE_KEYWORD
133
+ @group_id_stack.pop
134
+ end
140
135
  end
141
136
 
142
137
  private
@@ -161,7 +156,7 @@ module RubyLsp
161
156
  grouping_data = { group_id: @group_id_stack.last, kind: kind }
162
157
  grouping_data[:id] = @group_id if kind == :group
163
158
 
164
- @_response << create_code_lens(
159
+ @response_builder << create_code_lens(
165
160
  node,
166
161
  title: "Run",
167
162
  command_name: "rubyLsp.runTest",
@@ -169,7 +164,7 @@ module RubyLsp
169
164
  data: { type: "test", **grouping_data },
170
165
  )
171
166
 
172
- @_response << create_code_lens(
167
+ @response_builder << create_code_lens(
173
168
  node,
174
169
  title: "Run In Terminal",
175
170
  command_name: "rubyLsp.runTestInTerminal",
@@ -177,7 +172,7 @@ module RubyLsp
177
172
  data: { type: "test_in_terminal", **grouping_data },
178
173
  )
179
174
 
180
- @_response << create_code_lens(
175
+ @response_builder << create_code_lens(
181
176
  node,
182
177
  title: "Debug",
183
178
  command_name: "rubyLsp.debugTest",
@@ -186,29 +181,19 @@ module RubyLsp
186
181
  )
187
182
  end
188
183
 
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)
184
+ sig { params(group_name: String, method_name: T.nilable(String)).returns(String) }
185
+ def generate_test_command(group_name:, method_name: nil)
201
186
  command = BASE_COMMAND + T.must(@path)
202
187
 
203
188
  case DependencyDetector.instance.detected_test_library
204
189
  when "minitest"
205
190
  command += if method_name
206
- " --name " + "/#{Shellwords.escape(class_name + "#" + method_name)}/"
191
+ " --name " + "/#{Shellwords.escape(group_name + "#" + method_name)}/"
207
192
  else
208
- " --name " + "/#{Shellwords.escape(class_name)}/"
193
+ " --name " + "/#{Shellwords.escape(group_name)}/"
209
194
  end
210
195
  when "test-unit"
211
- command += " --testcase " + "/#{Shellwords.escape(class_name)}/"
196
+ command += " --testcase " + "/#{Shellwords.escape(group_name)}/"
212
197
 
213
198
  if method_name
214
199
  command += " --name " + Shellwords.escape(method_name)
@@ -218,14 +203,28 @@ module RubyLsp
218
203
  command
219
204
  end
220
205
 
221
- sig { params(node: Prism::CallNode, remote: String).void }
222
- def add_open_gem_remote_code_lens(node, remote)
223
- @_response << create_code_lens(
206
+ sig { params(node: Prism::CallNode, kind: Symbol).void }
207
+ def add_spec_code_lens(node, kind:)
208
+ arguments = node.arguments
209
+ return unless arguments
210
+
211
+ first_argument = arguments.arguments.first
212
+ return unless first_argument
213
+
214
+ name = case first_argument
215
+ when Prism::StringNode
216
+ first_argument.content
217
+ when Prism::ConstantReadNode
218
+ first_argument.full_name
219
+ end
220
+
221
+ return unless name
222
+
223
+ add_test_code_lens(
224
224
  node,
225
- title: "Open remote",
226
- command_name: "rubyLsp.openLink",
227
- arguments: [remote],
228
- data: { type: "link" },
225
+ name: name,
226
+ command: generate_test_command(group_name: name),
227
+ kind: kind,
229
228
  )
230
229
  end
231
230
  end
@@ -3,26 +3,21 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Listeners
6
- class Completion < Listener
6
+ class Completion
7
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
8
+ include Requests::Support::Common
14
9
 
15
10
  sig do
16
11
  params(
12
+ response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
17
13
  index: RubyIndexer::Index,
18
14
  nesting: T::Array[String],
19
15
  typechecker_enabled: T::Boolean,
20
16
  dispatcher: Prism::Dispatcher,
21
17
  ).void
22
18
  end
23
- def initialize(index, nesting, typechecker_enabled, dispatcher)
24
- super(dispatcher)
25
- @_response = T.let([], ResponseType)
19
+ def initialize(response_builder, index, nesting, typechecker_enabled, dispatcher)
20
+ @response_builder = response_builder
26
21
  @index = index
27
22
  @nesting = nesting
28
23
  @typechecker_enabled = typechecker_enabled
@@ -39,7 +34,7 @@ module RubyLsp
39
34
  sig { params(node: Prism::StringNode).void }
40
35
  def on_string_node_enter(node)
41
36
  @index.search_require_paths(node.content).map!(&:require_path).sort!.each do |path|
42
- @_response << build_completion(T.must(path), node)
37
+ @response_builder << build_completion(T.must(path), node)
43
38
  end
44
39
  end
45
40
 
@@ -48,11 +43,13 @@ module RubyLsp
48
43
  def on_constant_read_node_enter(node)
49
44
  return if DependencyDetector.instance.typechecker
50
45
 
51
- name = node.slice
46
+ name = constant_name(node)
47
+ return if name.nil?
48
+
52
49
  candidates = @index.prefix_search(name, @nesting)
53
50
  candidates.each do |entries|
54
51
  complete_name = T.must(entries.first).name
55
- @_response << build_entry_completion(
52
+ @response_builder << build_entry_completion(
56
53
  complete_name,
57
54
  name,
58
55
  node,
@@ -67,7 +64,8 @@ module RubyLsp
67
64
  def on_constant_path_node_enter(node)
68
65
  return if DependencyDetector.instance.typechecker
69
66
 
70
- name = node.slice
67
+ name = constant_name(node)
68
+ return if name.nil?
71
69
 
72
70
  top_level_reference = if name.start_with?("::")
73
71
  name = name.delete_prefix("::")
@@ -96,7 +94,7 @@ module RubyLsp
96
94
 
97
95
  full_name = aliased_namespace.empty? ? constant_name : "#{aliased_namespace}::#{constant_name}"
98
96
 
99
- @_response << build_entry_completion(
97
+ @response_builder << build_entry_completion(
100
98
  full_name,
101
99
  name,
102
100
  node,
@@ -123,7 +121,7 @@ module RubyLsp
123
121
  entry = entries.find { |e| e.is_a?(RubyIndexer::Entry::Member) && e.owner&.name == receiver.name }
124
122
  next unless entry
125
123
 
126
- @_response << build_method_completion(T.cast(entry, RubyIndexer::Entry::Member), node)
124
+ @response_builder << build_method_completion(T.cast(entry, RubyIndexer::Entry::Member), node)
127
125
  end
128
126
  end
129
127
 
@@ -147,7 +145,10 @@ module RubyLsp
147
145
  detail: "(#{entry.parameters.map(&:decorated_name).join(", ")})",
148
146
  description: entry.file_name,
149
147
  ),
150
- documentation: markdown_from_index_entries(name, entry),
148
+ documentation: Interface::MarkupContent.new(
149
+ kind: "markdown",
150
+ value: markdown_from_index_entries(name, entry),
151
+ ),
151
152
  )
152
153
  end
153
154
 
@@ -240,7 +241,10 @@ module RubyLsp
240
241
  label_details: Interface::CompletionItemLabelDetails.new(
241
242
  description: entries.map(&:file_name).join(","),
242
243
  ),
243
- documentation: markdown_from_index_entries(real_name, entries),
244
+ documentation: Interface::MarkupContent.new(
245
+ kind: "markdown",
246
+ value: markdown_from_index_entries(real_name, entries),
247
+ ),
244
248
  )
245
249
  end
246
250
 
@@ -3,17 +3,13 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Listeners
6
- class Definition < Listener
6
+ class Definition
7
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
8
+ include Requests::Support::Common
14
9
 
15
10
  sig do
16
11
  params(
12
+ response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
17
13
  uri: URI::Generic,
18
14
  nesting: T::Array[String],
19
15
  index: RubyIndexer::Index,
@@ -21,14 +17,12 @@ module RubyLsp
21
17
  typechecker_enabled: T::Boolean,
22
18
  ).void
23
19
  end
24
- def initialize(uri, nesting, index, dispatcher, typechecker_enabled)
20
+ def initialize(response_builder, uri, nesting, index, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
21
+ @response_builder = response_builder
25
22
  @uri = uri
26
23
  @nesting = nesting
27
24
  @index = index
28
25
  @typechecker_enabled = typechecker_enabled
29
- @_response = T.let(nil, ResponseType)
30
-
31
- super(dispatcher)
32
26
 
33
27
  dispatcher.register(
34
28
  self,
@@ -51,12 +45,18 @@ module RubyLsp
51
45
 
52
46
  sig { params(node: Prism::ConstantPathNode).void }
53
47
  def on_constant_path_node_enter(node)
54
- find_in_index(node.slice)
48
+ name = constant_name(node)
49
+ return if name.nil?
50
+
51
+ find_in_index(name)
55
52
  end
56
53
 
57
54
  sig { params(node: Prism::ConstantReadNode).void }
58
55
  def on_constant_read_node_enter(node)
59
- find_in_index(node.slice)
56
+ name = constant_name(node)
57
+ return if name.nil?
58
+
59
+ find_in_index(name)
60
60
  end
61
61
 
62
62
  private
@@ -68,20 +68,22 @@ module RubyLsp
68
68
  message = node.message
69
69
  return unless message
70
70
 
71
- target_method = @index.resolve_method(message, @nesting.join("::"))
72
- return unless target_method
71
+ methods = @index.resolve_method(message, @nesting.join("::"))
72
+ return unless methods
73
73
 
74
- location = target_method.location
75
- file_path = target_method.file_path
76
- return if @typechecker_enabled && not_in_dependencies?(file_path)
74
+ methods.each do |target_method|
75
+ location = target_method.location
76
+ file_path = target_method.file_path
77
+ next if @typechecker_enabled && not_in_dependencies?(file_path)
77
78
 
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
- )
79
+ @response_builder << Interface::Location.new(
80
+ uri: URI::Generic.from_path(path: file_path).to_s,
81
+ range: Interface::Range.new(
82
+ start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
83
+ end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
84
+ ),
85
+ )
86
+ end
85
87
  end
86
88
 
87
89
  sig { params(node: Prism::CallNode).void }
@@ -102,7 +104,7 @@ module RubyLsp
102
104
  if entry
103
105
  candidate = entry.full_path
104
106
 
105
- @_response = Interface::Location.new(
107
+ @response_builder << Interface::Location.new(
106
108
  uri: URI::Generic.from_path(path: candidate).to_s,
107
109
  range: Interface::Range.new(
108
110
  start: Interface::Position.new(line: 0, character: 0),
@@ -116,7 +118,7 @@ module RubyLsp
116
118
  current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : Dir.pwd
117
119
  candidate = File.expand_path(File.join(current_folder, required_file))
118
120
 
119
- @_response = Interface::Location.new(
121
+ @response_builder << Interface::Location.new(
120
122
  uri: URI::Generic.from_path(path: candidate).to_s,
121
123
  range: Interface::Range.new(
122
124
  start: Interface::Position.new(line: 0, character: 0),
@@ -136,7 +138,7 @@ module RubyLsp
136
138
  first_entry = T.must(entries.first)
137
139
  return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{value}"
138
140
 
139
- @_response = entries.filter_map do |entry|
141
+ entries.each do |entry|
140
142
  location = entry.location
141
143
  # If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
142
144
  # additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants
@@ -144,7 +146,7 @@ module RubyLsp
144
146
  file_path = entry.file_path
145
147
  next if @typechecker_enabled && not_in_dependencies?(file_path)
146
148
 
147
- Interface::Location.new(
149
+ @response_builder << Interface::Location.new(
148
150
  uri: URI::Generic.from_path(path: file_path).to_s,
149
151
  range: Interface::Range.new(
150
152
  start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
@@ -3,10 +3,9 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Listeners
6
- class DocumentHighlight < Listener
6
+ class DocumentHighlight
7
7
  extend T::Sig
8
-
9
- ResponseType = type_member { { fixed: T::Array[Interface::DocumentHighlight] } }
8
+ include Requests::Support::Common
10
9
 
11
10
  GLOBAL_VARIABLE_NODES = T.let(
12
11
  [
@@ -87,20 +86,16 @@ module RubyLsp
87
86
  T::Array[T.class_of(Prism::Node)],
88
87
  )
89
88
 
90
- sig { override.returns(ResponseType) }
91
- attr_reader :_response
92
-
93
89
  sig do
94
90
  params(
91
+ response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::DocumentHighlight],
95
92
  target: T.nilable(Prism::Node),
96
93
  parent: T.nilable(Prism::Node),
97
94
  dispatcher: Prism::Dispatcher,
98
95
  ).void
99
96
  end
100
- def initialize(target, parent, dispatcher)
101
- super(dispatcher)
102
-
103
- @_response = T.let([], T::Array[Interface::DocumentHighlight])
97
+ def initialize(response_builder, target, parent, dispatcher)
98
+ @response_builder = response_builder
104
99
 
105
100
  return unless target && parent
106
101
 
@@ -521,7 +516,7 @@ module RubyLsp
521
516
 
522
517
  sig { params(kind: Integer, location: Prism::Location).void }
523
518
  def add_highlight(kind, location)
524
- @_response << Interface::DocumentHighlight.new(range: range_from_location(location), kind: kind)
519
+ @response_builder << Interface::DocumentHighlight.new(range: range_from_location(location), kind: kind)
525
520
  end
526
521
 
527
522
  sig { params(node: T.nilable(Prism::Node)).returns(T.nilable(String)) }
@@ -5,11 +5,9 @@ require "ruby_lsp/requests/support/source_uri"
5
5
 
6
6
  module RubyLsp
7
7
  module Listeners
8
- class DocumentLink < Listener
8
+ class DocumentLink
9
9
  extend T::Sig
10
- extend T::Generic
11
-
12
- ResponseType = type_member { { fixed: T::Array[Interface::DocumentLink] } }
10
+ include Requests::Support::Common
13
11
 
14
12
  GEM_TO_VERSION_MAP = T.let(
15
13
  [*::Gem::Specification.default_stubs, *::Gem::Specification.stubs].map! do |s|
@@ -59,25 +57,21 @@ module RubyLsp
59
57
  end
60
58
  end
61
59
 
62
- sig { override.returns(ResponseType) }
63
- attr_reader :_response
64
-
65
60
  sig do
66
61
  params(
62
+ response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::DocumentLink],
67
63
  uri: URI::Generic,
68
64
  comments: T::Array[Prism::Comment],
69
65
  dispatcher: Prism::Dispatcher,
70
66
  ).void
71
67
  end
72
- def initialize(uri, comments, dispatcher)
73
- super(dispatcher)
74
-
68
+ def initialize(response_builder, uri, comments, dispatcher)
75
69
  # Match the version based on the version in the RBI file name. Notice that the `@` symbol is sanitized to `%40`
76
70
  # in the URI
71
+ @response_builder = response_builder
77
72
  path = uri.to_standardized_path
78
73
  version_match = path ? /(?<=%40)[\d.]+(?=\.rbi$)/.match(path) : nil
79
74
  @gem_version = T.let(version_match && version_match[0], T.nilable(String))
80
- @_response = T.let([], T::Array[Interface::DocumentLink])
81
75
  @lines_to_comments = T.let(
82
76
  comments.to_h do |comment|
83
77
  [comment.location.end_line, comment]
@@ -137,7 +131,7 @@ module RubyLsp
137
131
  file_path = self.class.gem_paths.dig(uri.gem_name, gem_version, CGI.unescape(uri.path))
138
132
  return if file_path.nil?
139
133
 
140
- @_response << Interface::DocumentLink.new(
134
+ @response_builder << Interface::DocumentLink.new(
141
135
  range: range_from_location(comment.location),
142
136
  target: "file://#{file_path}##{uri.line_number}",
143
137
  tooltip: "Jump to #{file_path}##{uri.line_number}",