ruby-lsp 0.19.0 → 0.19.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1d89f4c5d5724a4b8c17002986c64d9f3ea2951282e4a35d96d50b0dd699ba9
4
- data.tar.gz: f3513888e58fbde4a85d93a2ef58ae124e02c354729693ae5e928a66548cd058
3
+ metadata.gz: d900634d5328c8b4b5fcf799f807456bec3cdb94d6294c80fa9152bcf45aedde
4
+ data.tar.gz: b7661867d38331ec158312b10c7f530cbd58994ee94e8ff592f478e7e05efcb7
5
5
  SHA512:
6
- metadata.gz: 57b44fec7c55b57408e042a9c40fe900c2445aa9fefa811223aa3154f5a4e1e8b1d0f11cf306548b13653f3a61877bcb8d01df7beeadb4766f2094b27786653b
7
- data.tar.gz: cf769828221a3a1cb65e906b2b9c5c0c8fac8feee265e5f31b90fb96813f10a225798da1b3746165e11fd4591f7bd0620ab177233c2214d939986e22a1e3709b
6
+ metadata.gz: 8f7955f676bc5f231f990f04d8aea1a767165d4f2cac1e9514928014cd8a09eb68c60b4cd226a94b554dac88d77c2117ae3a3ea76bb66e68980974f1685d8970
7
+ data.tar.gz: 1b914d5607e5730139780cd1f172923db7878a7a0a4098daa1e17df8a1a7a2d8b6731b092baf7eeefc9f55605305e4d70d8cca8489f68a17bab532026c38e8b2
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.19.0
1
+ 0.19.1
data/exe/ruby-lsp-check CHANGED
@@ -32,7 +32,7 @@ files.each_with_index do |file, index|
32
32
  })
33
33
 
34
34
  result = server.pop_response
35
- errors[file] = result.message if result.is_a?(RubyLsp::Error)
35
+ errors[file] = result if result.is_a?(RubyLsp::Error)
36
36
  ensure
37
37
  server.process_message({ method: "textDocument/didClose", params: { textDocument: { uri: uri } } })
38
38
  server.pop_response
@@ -110,6 +110,10 @@ module RubyIndexer
110
110
  else
111
111
  ""
112
112
  end
113
+ rescue Errno::ENOENT
114
+ # If the file was deleted, but the entry hasn't been removed yet (could happen due to concurrency), then we do
115
+ # not want to fail. Just set the comments to an empty string
116
+ ""
113
117
  end
114
118
  end
115
119
 
@@ -525,6 +529,9 @@ module RubyIndexer
525
529
  end
526
530
  end
527
531
 
532
+ # Represents a global variable e.g.: $DEBUG
533
+ class GlobalVariable < Entry; end
534
+
528
535
  # Represents an instance variable e.g.: @a = 1
529
536
  class InstanceVariable < Entry
530
537
  sig { returns(T.nilable(Entry::Namespace)) }
@@ -677,11 +677,13 @@ module RubyIndexer
677
677
  sig do
678
678
  type_parameters(:T).params(
679
679
  path: String,
680
- type: T::Class[T.all(T.type_parameter(:T), Entry)],
681
- ).returns(T.nilable(T::Array[T.type_parameter(:T)]))
680
+ type: T.nilable(T::Class[T.all(T.type_parameter(:T), Entry)]),
681
+ ).returns(T.nilable(T.any(T::Array[Entry], T::Array[T.type_parameter(:T)])))
682
682
  end
683
- def entries_for(path, type)
683
+ def entries_for(path, type = nil)
684
684
  entries = @files_to_entries[path]
685
+ return entries unless type
686
+
685
687
  entries&.grep(type)
686
688
  end
687
689
 
@@ -44,6 +44,8 @@ module RubyIndexer
44
44
  when RBS::AST::Declarations::Constant
45
45
  namespace_nesting = declaration.name.namespace.path.map(&:to_s)
46
46
  handle_constant(declaration, namespace_nesting, pathname.to_s)
47
+ when RBS::AST::Declarations::Global
48
+ handle_global_variable(declaration, pathname)
47
49
  else # rubocop:disable Style/EmptyElse
48
50
  # Other kinds not yet handled
49
51
  end
@@ -271,6 +273,23 @@ module RubyIndexer
271
273
  ))
272
274
  end
273
275
 
276
+ sig { params(declaration: RBS::AST::Declarations::Global, pathname: Pathname).void }
277
+ def handle_global_variable(declaration, pathname)
278
+ name = declaration.name.to_s
279
+ file_path = pathname.to_s
280
+ location = to_ruby_indexer_location(declaration.location)
281
+ comments = comments_to_string(declaration)
282
+ encoding = @index.configuration.encoding
283
+
284
+ @index.add(Entry::GlobalVariable.new(
285
+ name,
286
+ file_path,
287
+ location,
288
+ comments,
289
+ encoding,
290
+ ))
291
+ end
292
+
274
293
  sig { params(member: RBS::AST::Members::Alias, owner_entry: Entry::Namespace).void }
275
294
  def handle_signature_alias(member, owner_entry)
276
295
  file_path = member.location.buffer.name
@@ -294,6 +313,7 @@ module RubyIndexer
294
313
  RBS::AST::Declarations::Class,
295
314
  RBS::AST::Declarations::Module,
296
315
  RBS::AST::Declarations::Constant,
316
+ RBS::AST::Declarations::Global,
297
317
  RBS::AST::Members::MethodDefinition,
298
318
  RBS::AST::Members::Alias,
299
319
  )).returns(T.nilable(String))
@@ -5,6 +5,38 @@ module RubyIndexer
5
5
  class ReferenceFinder
6
6
  extend T::Sig
7
7
 
8
+ class Target
9
+ extend T::Helpers
10
+
11
+ abstract!
12
+ end
13
+
14
+ class ConstTarget < Target
15
+ extend T::Sig
16
+
17
+ sig { returns(String) }
18
+ attr_reader :fully_qualified_name
19
+
20
+ sig { params(fully_qualified_name: String).void }
21
+ def initialize(fully_qualified_name)
22
+ super()
23
+ @fully_qualified_name = fully_qualified_name
24
+ end
25
+ end
26
+
27
+ class MethodTarget < Target
28
+ extend T::Sig
29
+
30
+ sig { returns(String) }
31
+ attr_reader :method_name
32
+
33
+ sig { params(method_name: String).void }
34
+ def initialize(method_name)
35
+ super()
36
+ @method_name = method_name
37
+ end
38
+ end
39
+
8
40
  class Reference
9
41
  extend T::Sig
10
42
 
@@ -14,26 +46,29 @@ module RubyIndexer
14
46
  sig { returns(Prism::Location) }
15
47
  attr_reader :location
16
48
 
17
- sig { params(name: String, location: Prism::Location).void }
18
- def initialize(name, location)
49
+ sig { returns(T::Boolean) }
50
+ attr_reader :declaration
51
+
52
+ sig { params(name: String, location: Prism::Location, declaration: T::Boolean).void }
53
+ def initialize(name, location, declaration:)
19
54
  @name = name
20
55
  @location = location
56
+ @declaration = declaration
21
57
  end
22
58
  end
23
59
 
24
- sig { returns(T::Array[Reference]) }
25
- attr_reader :references
26
-
27
60
  sig do
28
61
  params(
29
- fully_qualified_name: String,
62
+ target: Target,
30
63
  index: RubyIndexer::Index,
31
64
  dispatcher: Prism::Dispatcher,
65
+ include_declarations: T::Boolean,
32
66
  ).void
33
67
  end
34
- def initialize(fully_qualified_name, index, dispatcher)
35
- @fully_qualified_name = fully_qualified_name
68
+ def initialize(target, index, dispatcher, include_declarations: true)
69
+ @target = target
36
70
  @index = index
71
+ @include_declarations = include_declarations
37
72
  @stack = T.let([], T::Array[String])
38
73
  @references = T.let([], T::Array[Reference])
39
74
 
@@ -59,17 +94,25 @@ module RubyIndexer
59
94
  :on_constant_or_write_node_enter,
60
95
  :on_constant_and_write_node_enter,
61
96
  :on_constant_operator_write_node_enter,
97
+ :on_call_node_enter,
62
98
  )
63
99
  end
64
100
 
101
+ sig { returns(T::Array[Reference]) }
102
+ def references
103
+ return @references if @include_declarations
104
+
105
+ @references.reject(&:declaration)
106
+ end
107
+
65
108
  sig { params(node: Prism::ClassNode).void }
66
109
  def on_class_node_enter(node)
67
110
  constant_path = node.constant_path
68
111
  name = constant_path.slice
69
112
  nesting = actual_nesting(name)
70
113
 
71
- if nesting.join("::") == @fully_qualified_name
72
- @references << Reference.new(name, constant_path.location)
114
+ if @target.is_a?(ConstTarget) && nesting.join("::") == @target.fully_qualified_name
115
+ @references << Reference.new(name, constant_path.location, declaration: true)
73
116
  end
74
117
 
75
118
  @stack << name
@@ -86,8 +129,8 @@ module RubyIndexer
86
129
  name = constant_path.slice
87
130
  nesting = actual_nesting(name)
88
131
 
89
- if nesting.join("::") == @fully_qualified_name
90
- @references << Reference.new(name, constant_path.location)
132
+ if @target.is_a?(ConstTarget) && nesting.join("::") == @target.fully_qualified_name
133
+ @references << Reference.new(name, constant_path.location, declaration: true)
91
134
  end
92
135
 
93
136
  @stack << name
@@ -203,6 +246,10 @@ module RubyIndexer
203
246
 
204
247
  sig { params(node: Prism::DefNode).void }
205
248
  def on_def_node_enter(node)
249
+ if @target.is_a?(MethodTarget) && (name = node.name.to_s) == @target.method_name
250
+ @references << Reference.new(name, node.name_loc, declaration: true)
251
+ end
252
+
206
253
  if node.receiver.is_a?(Prism::SelfNode)
207
254
  @stack << "<Class:#{@stack.last}>"
208
255
  end
@@ -215,6 +262,13 @@ module RubyIndexer
215
262
  end
216
263
  end
217
264
 
265
+ sig { params(node: Prism::CallNode).void }
266
+ def on_call_node_enter(node)
267
+ if @target.is_a?(MethodTarget) && (name = node.name.to_s) == @target.method_name
268
+ @references << Reference.new(name, T.must(node.message_loc), declaration: false)
269
+ end
270
+ end
271
+
218
272
  private
219
273
 
220
274
  sig { params(name: String).returns(T::Array[String]) }
@@ -233,13 +287,21 @@ module RubyIndexer
233
287
 
234
288
  sig { params(name: String, location: Prism::Location).void }
235
289
  def collect_constant_references(name, location)
290
+ return unless @target.is_a?(ConstTarget)
291
+
236
292
  entries = @index.resolve(name, @stack)
237
293
  return unless entries
238
294
 
295
+ previous_reference = @references.last
296
+
239
297
  entries.each do |entry|
240
- next unless entry.name == @fully_qualified_name
298
+ next unless entry.name == @target.fully_qualified_name
299
+
300
+ # When processing a class/module declaration, we eagerly handle the constant reference. To avoid duplicates,
301
+ # when we find the constant node defining the namespace, then we have to check if it wasn't already added
302
+ next if previous_reference&.location == location
241
303
 
242
- @references << Reference.new(name, location)
304
+ @references << Reference.new(name, location, declaration: false)
243
305
  end
244
306
  end
245
307
 
@@ -630,5 +630,17 @@ module RubyIndexer
630
630
  its namespace nesting, and the surrounding CallNode (e.g. a method call).
631
631
  COMMENTS
632
632
  end
633
+
634
+ def test_lazy_comment_fetching_does_not_fail_if_file_gets_deleted
635
+ indexable = IndexablePath.new("#{Dir.pwd}/lib", "lib/ruby_lsp/does_not_exist.rb")
636
+
637
+ @index.index_single(indexable, <<~RUBY, collect_comments: false)
638
+ class Foo
639
+ end
640
+ RUBY
641
+
642
+ entry = @index["Foo"].first
643
+ assert_empty(entry.comments)
644
+ end
633
645
  end
634
646
  end
@@ -1858,6 +1858,9 @@ module RubyIndexer
1858
1858
 
1859
1859
  entries = @index.entries_for("/fake/path/foo.rb", RubyIndexer::Entry::Namespace)
1860
1860
  assert_equal(["Foo", "Bar", "Bar::<Class:Bar>"], entries.map(&:name))
1861
+
1862
+ entries = @index.entries_for("/fake/path/foo.rb")
1863
+ assert_equal(["Foo", "Bar", "my_def", "Bar::<Class:Bar>", "my_singleton_def"], entries.map(&:name))
1861
1864
  end
1862
1865
 
1863
1866
  def test_entries_for_returns_nil_if_no_matches
@@ -76,6 +76,20 @@ module RubyIndexer
76
76
  assert_operator(entry.location.end_column, :>, 0)
77
77
  end
78
78
 
79
+ def test_index_global_declaration
80
+ entries = @index["$DEBUG"]
81
+ refute_nil(entries)
82
+ assert_equal(1, entries.length)
83
+
84
+ entry = entries.first
85
+
86
+ assert_instance_of(Entry::GlobalVariable, entry)
87
+ assert_equal("$DEBUG", entry.name)
88
+ assert_match(%r{/gems/rbs-.*/core/global_variables.rbs}, entry.file_path)
89
+ assert_operator(entry.location.start_column, :<, entry.location.end_column)
90
+ assert_equal(entry.location.start_line, entry.location.end_line)
91
+ end
92
+
79
93
  def test_attaches_correct_owner_to_singleton_methods
80
94
  entries = @index["basename"]
81
95
  refute_nil(entries)
@@ -6,7 +6,7 @@ require "test_helper"
6
6
  module RubyIndexer
7
7
  class ReferenceFinderTest < Minitest::Test
8
8
  def test_finds_constant_references
9
- refs = find_references("Foo::Bar", <<~RUBY)
9
+ refs = find_const_references("Foo::Bar", <<~RUBY)
10
10
  module Foo
11
11
  class Bar
12
12
  end
@@ -28,7 +28,7 @@ module RubyIndexer
28
28
  end
29
29
 
30
30
  def test_finds_constant_references_inside_singleton_contexts
31
- refs = find_references("Foo::<Class:Foo>::Bar", <<~RUBY)
31
+ refs = find_const_references("Foo::<Class:Foo>::Bar", <<~RUBY)
32
32
  class Foo
33
33
  class << self
34
34
  class Bar
@@ -47,7 +47,7 @@ module RubyIndexer
47
47
  end
48
48
 
49
49
  def test_finds_top_level_constant_references
50
- refs = find_references("Bar", <<~RUBY)
50
+ refs = find_const_references("Bar", <<~RUBY)
51
51
  class Bar
52
52
  end
53
53
 
@@ -70,17 +70,173 @@ module RubyIndexer
70
70
  assert_equal(8, refs[2].location.start_line)
71
71
  end
72
72
 
73
+ def test_finds_method_references
74
+ refs = find_method_references("foo", <<~RUBY)
75
+ class Bar
76
+ def foo
77
+ end
78
+
79
+ def baz
80
+ foo
81
+ end
82
+ end
83
+ RUBY
84
+
85
+ assert_equal(2, refs.size)
86
+
87
+ assert_equal("foo", refs[0].name)
88
+ assert_equal(2, refs[0].location.start_line)
89
+
90
+ assert_equal("foo", refs[1].name)
91
+ assert_equal(6, refs[1].location.start_line)
92
+ end
93
+
94
+ def test_does_not_mismatch_on_readers_and_writers
95
+ refs = find_method_references("foo", <<~RUBY)
96
+ class Bar
97
+ def foo
98
+ end
99
+
100
+ def foo=(value)
101
+ end
102
+
103
+ def baz
104
+ self.foo = 1
105
+ self.foo
106
+ end
107
+ end
108
+ RUBY
109
+
110
+ # We want to match `foo` but not `foo=`
111
+ assert_equal(2, refs.size)
112
+
113
+ assert_equal("foo", refs[0].name)
114
+ assert_equal(2, refs[0].location.start_line)
115
+
116
+ assert_equal("foo", refs[1].name)
117
+ assert_equal(10, refs[1].location.start_line)
118
+ end
119
+
120
+ def test_matches_writers
121
+ refs = find_method_references("foo=", <<~RUBY)
122
+ class Bar
123
+ def foo
124
+ end
125
+
126
+ def foo=(value)
127
+ end
128
+
129
+ def baz
130
+ self.foo = 1
131
+ self.foo
132
+ end
133
+ end
134
+ RUBY
135
+
136
+ # We want to match `foo=` but not `foo`
137
+ assert_equal(2, refs.size)
138
+
139
+ assert_equal("foo=", refs[0].name)
140
+ assert_equal(5, refs[0].location.start_line)
141
+
142
+ assert_equal("foo=", refs[1].name)
143
+ assert_equal(9, refs[1].location.start_line)
144
+ end
145
+
146
+ def test_find_inherited_methods
147
+ refs = find_method_references("foo", <<~RUBY)
148
+ class Bar
149
+ def foo
150
+ end
151
+ end
152
+
153
+ class Baz < Bar
154
+ super.foo
155
+ end
156
+ RUBY
157
+
158
+ assert_equal(2, refs.size)
159
+
160
+ assert_equal("foo", refs[0].name)
161
+ assert_equal(2, refs[0].location.start_line)
162
+
163
+ assert_equal("foo", refs[1].name)
164
+ assert_equal(7, refs[1].location.start_line)
165
+ end
166
+
167
+ def test_finds_methods_created_in_mixins
168
+ refs = find_method_references("foo", <<~RUBY)
169
+ module Mixin
170
+ def foo
171
+ end
172
+ end
173
+
174
+ class Bar
175
+ include Mixin
176
+ end
177
+
178
+ Bar.foo
179
+ RUBY
180
+
181
+ assert_equal(2, refs.size)
182
+
183
+ assert_equal("foo", refs[0].name)
184
+ assert_equal(2, refs[0].location.start_line)
185
+
186
+ assert_equal("foo", refs[1].name)
187
+ assert_equal(10, refs[1].location.start_line)
188
+ end
189
+
190
+ def test_finds_singleton_methods
191
+ # The current implementation matches on both `Bar.foo` and `Bar#foo` even though they are different
192
+
193
+ refs = find_method_references("foo", <<~RUBY)
194
+ class Bar
195
+ class << self
196
+ def foo
197
+ end
198
+ end
199
+
200
+ def foo
201
+ end
202
+ end
203
+
204
+ Bar.foo
205
+ RUBY
206
+
207
+ assert_equal(3, refs.size)
208
+
209
+ assert_equal("foo", refs[0].name)
210
+ assert_equal(3, refs[0].location.start_line)
211
+
212
+ assert_equal("foo", refs[1].name)
213
+ assert_equal(7, refs[1].location.start_line)
214
+
215
+ assert_equal("foo", refs[2].name)
216
+ assert_equal(11, refs[2].location.start_line)
217
+ end
218
+
73
219
  private
74
220
 
75
- def find_references(fully_qualified_name, source)
221
+ def find_const_references(const_name, source)
222
+ target = ReferenceFinder::ConstTarget.new(const_name)
223
+ find_references(target, source)
224
+ end
225
+
226
+ def find_method_references(method_name, source)
227
+ target = ReferenceFinder::MethodTarget.new(method_name)
228
+ find_references(target, source)
229
+ end
230
+
231
+ def find_references(target, source)
76
232
  file_path = "/fake.rb"
77
233
  index = Index.new
78
234
  index.index_single(IndexablePath.new(nil, file_path), source)
79
235
  parse_result = Prism.parse(source)
80
236
  dispatcher = Prism::Dispatcher.new
81
- finder = ReferenceFinder.new(fully_qualified_name, index, dispatcher)
237
+ finder = ReferenceFinder.new(target, index, dispatcher)
82
238
  dispatcher.visit(parse_result.value)
83
- finder.references.uniq(&:location)
239
+ finder.references
84
240
  end
85
241
  end
86
242
  end
@@ -50,7 +50,12 @@ module RubyLsp
50
50
  ).returns(NodeContext)
51
51
  end
52
52
  def locate_node(position, node_types: [])
53
- RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
53
+ RubyDocument.locate(
54
+ @parse_result.value,
55
+ create_scanner.find_char_position(position),
56
+ node_types: node_types,
57
+ encoding: @encoding,
58
+ )
54
59
  end
55
60
 
56
61
  sig { params(char_position: Integer).returns(T.nilable(T::Boolean)) }
@@ -75,10 +75,12 @@ require "ruby_lsp/requests/hover"
75
75
  require "ruby_lsp/requests/inlay_hints"
76
76
  require "ruby_lsp/requests/on_type_formatting"
77
77
  require "ruby_lsp/requests/prepare_type_hierarchy"
78
+ require "ruby_lsp/requests/range_formatting"
79
+ require "ruby_lsp/requests/references"
80
+ require "ruby_lsp/requests/rename"
78
81
  require "ruby_lsp/requests/selection_ranges"
79
82
  require "ruby_lsp/requests/semantic_highlighting"
80
83
  require "ruby_lsp/requests/show_syntax_tree"
81
84
  require "ruby_lsp/requests/signature_help"
82
85
  require "ruby_lsp/requests/type_hierarchy_supertypes"
83
86
  require "ruby_lsp/requests/workspace_symbol"
84
- require "ruby_lsp/requests/rename"
@@ -23,10 +23,11 @@ module RubyLsp
23
23
  end
24
24
  end
25
25
 
26
- sig { params(document: RubyDocument, code_action: T::Hash[Symbol, T.untyped]).void }
27
- def initialize(document, code_action)
26
+ sig { params(document: RubyDocument, global_state: GlobalState, code_action: T::Hash[Symbol, T.untyped]).void }
27
+ def initialize(document, global_state, code_action)
28
28
  super()
29
29
  @document = document
30
+ @global_state = global_state
30
31
  @code_action = code_action
31
32
  end
32
33
 
@@ -191,7 +192,12 @@ module RubyLsp
191
192
  extracted_source = T.must(@document.source[start_index...end_index])
192
193
 
193
194
  # Find the closest method declaration node, so that we place the refactor in a valid position
194
- node_context = RubyDocument.locate(@document.parse_result.value, start_index, node_types: [Prism::DefNode])
195
+ node_context = RubyDocument.locate(
196
+ @document.parse_result.value,
197
+ start_index,
198
+ node_types: [Prism::DefNode],
199
+ encoding: @global_state.encoding,
200
+ )
195
201
  closest_node = node_context.node
196
202
  return Error::InvalidTargetRange unless closest_node
197
203
 
@@ -57,6 +57,7 @@ module RubyLsp
57
57
  Prism::InstanceVariableTargetNode,
58
58
  Prism::InstanceVariableWriteNode,
59
59
  ],
60
+ encoding: global_state.encoding,
60
61
  )
61
62
  @response_builder = T.let(
62
63
  ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem].new,
@@ -57,6 +57,7 @@ module RubyLsp
57
57
  Prism::SuperNode,
58
58
  Prism::ForwardingSuperNode,
59
59
  ],
60
+ encoding: global_state.encoding,
60
61
  )
61
62
 
62
63
  target = node_context.node
@@ -28,7 +28,7 @@ module RubyLsp
28
28
  char_position = document.create_scanner.find_char_position(position)
29
29
  delegate_request_if_needed!(global_state, document, char_position)
30
30
 
31
- node_context = RubyDocument.locate(document.parse_result.value, char_position)
31
+ node_context = RubyDocument.locate(document.parse_result.value, char_position, encoding: global_state.encoding)
32
32
 
33
33
  @response_builder = T.let(
34
34
  ResponseBuilders::CollectionResponseBuilder[Interface::DocumentHighlight].new,
@@ -41,6 +41,7 @@ module RubyLsp
41
41
  document.parse_result.value,
42
42
  char_position,
43
43
  node_types: Listeners::Hover::ALLOWED_TARGETS,
44
+ encoding: global_state.encoding,
44
45
  )
45
46
  target = node_context.node
46
47
  parent = node_context.parent
@@ -0,0 +1,55 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # The [range formatting](https://microsoft.github.io/language-server-protocol/specification#textDocument_rangeFormatting)
7
+ # is used to format a selection or to format on paste.
8
+ class RangeFormatting < Request
9
+ extend T::Sig
10
+
11
+ sig { params(global_state: GlobalState, document: RubyDocument, params: T::Hash[Symbol, T.untyped]).void }
12
+ def initialize(global_state, document, params)
13
+ super()
14
+ @document = document
15
+ @uri = T.let(document.uri, URI::Generic)
16
+ @params = params
17
+ @active_formatter = T.let(global_state.active_formatter, T.nilable(Support::Formatter))
18
+ end
19
+
20
+ sig { override.returns(T.nilable(T::Array[Interface::TextEdit])) }
21
+ def perform
22
+ return unless @active_formatter
23
+ return if @document.syntax_error?
24
+
25
+ target = @document.locate_first_within_range(@params[:range])
26
+ return unless target
27
+
28
+ location = target.location
29
+
30
+ formatted_text = @active_formatter.run_range_formatting(
31
+ @uri,
32
+ target.slice,
33
+ location.start_column / 2,
34
+ )
35
+ return unless formatted_text
36
+
37
+ [
38
+ Interface::TextEdit.new(
39
+ range: Interface::Range.new(
40
+ start: Interface::Position.new(
41
+ line: location.start_line - 1,
42
+ character: location.start_code_units_column(@document.encoding),
43
+ ),
44
+ end: Interface::Position.new(
45
+ line: location.end_line - 1,
46
+ character: location.end_code_units_column(@document.encoding),
47
+ ),
48
+ ),
49
+ new_text: formatted_text.strip,
50
+ ),
51
+ ]
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,146 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # The
7
+ # [references](https://microsoft.github.io/language-server-protocol/specification#textDocument_references)
8
+ # request finds all references for the selected symbol.
9
+ class References < Request
10
+ extend T::Sig
11
+ include Support::Common
12
+
13
+ sig do
14
+ params(
15
+ global_state: GlobalState,
16
+ store: Store,
17
+ document: T.any(RubyDocument, ERBDocument),
18
+ params: T::Hash[Symbol, T.untyped],
19
+ ).void
20
+ end
21
+ def initialize(global_state, store, document, params)
22
+ super()
23
+ @global_state = global_state
24
+ @store = store
25
+ @document = document
26
+ @params = params
27
+ @locations = T.let([], T::Array[Interface::Location])
28
+ end
29
+
30
+ sig { override.returns(T::Array[Interface::Location]) }
31
+ def perform
32
+ position = @params[:position]
33
+ char_position = @document.create_scanner.find_char_position(position)
34
+
35
+ node_context = RubyDocument.locate(
36
+ @document.parse_result.value,
37
+ char_position,
38
+ node_types: [
39
+ Prism::ConstantReadNode,
40
+ Prism::ConstantPathNode,
41
+ Prism::ConstantPathTargetNode,
42
+ Prism::CallNode,
43
+ Prism::DefNode,
44
+ ],
45
+ encoding: @global_state.encoding,
46
+ )
47
+ target = node_context.node
48
+ parent = node_context.parent
49
+ return @locations if !target || target.is_a?(Prism::ProgramNode)
50
+
51
+ if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
52
+ target = determine_target(
53
+ target,
54
+ parent,
55
+ position,
56
+ )
57
+ end
58
+
59
+ target = T.cast(
60
+ target,
61
+ T.any(
62
+ Prism::ConstantReadNode,
63
+ Prism::ConstantPathNode,
64
+ Prism::ConstantPathTargetNode,
65
+ Prism::CallNode,
66
+ Prism::DefNode,
67
+ ),
68
+ )
69
+
70
+ reference_target = create_reference_target(target, node_context)
71
+ return @locations unless reference_target
72
+
73
+ Dir.glob(File.join(@global_state.workspace_path, "**/*.rb")).each do |path|
74
+ uri = URI::Generic.from_path(path: path)
75
+ # If the document is being managed by the client, then we should use whatever is present in the store instead
76
+ # of reading from disk
77
+ next if @store.key?(uri)
78
+
79
+ parse_result = Prism.parse_file(path)
80
+ collect_references(reference_target, parse_result, uri)
81
+ end
82
+
83
+ @store.each do |_uri, document|
84
+ collect_references(reference_target, document.parse_result, document.uri)
85
+ end
86
+
87
+ @locations
88
+ end
89
+
90
+ private
91
+
92
+ sig do
93
+ params(
94
+ target_node: T.any(
95
+ Prism::ConstantReadNode,
96
+ Prism::ConstantPathNode,
97
+ Prism::ConstantPathTargetNode,
98
+ Prism::CallNode,
99
+ Prism::DefNode,
100
+ ),
101
+ node_context: NodeContext,
102
+ ).returns(T.nilable(RubyIndexer::ReferenceFinder::Target))
103
+ end
104
+ def create_reference_target(target_node, node_context)
105
+ case target_node
106
+ when Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode
107
+ name = constant_name(target_node)
108
+ return unless name
109
+
110
+ entries = @global_state.index.resolve(name, node_context.nesting)
111
+ return unless entries
112
+
113
+ fully_qualified_name = T.must(entries.first).name
114
+ RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name)
115
+ when Prism::CallNode, Prism::DefNode
116
+ RubyIndexer::ReferenceFinder::MethodTarget.new(target_node.name.to_s)
117
+ end
118
+ end
119
+
120
+ sig do
121
+ params(
122
+ target: RubyIndexer::ReferenceFinder::Target,
123
+ parse_result: Prism::ParseResult,
124
+ uri: URI::Generic,
125
+ ).void
126
+ end
127
+ def collect_references(target, parse_result, uri)
128
+ dispatcher = Prism::Dispatcher.new
129
+ finder = RubyIndexer::ReferenceFinder.new(
130
+ target,
131
+ @global_state.index,
132
+ dispatcher,
133
+ include_declarations: @params.dig(:context, :includeDeclaration) || true,
134
+ )
135
+ dispatcher.visit(parse_result.value)
136
+
137
+ finder.references.each do |reference|
138
+ @locations << Interface::Location.new(
139
+ uri: uri.to_s,
140
+ range: range_from_location(reference.location),
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -37,6 +37,7 @@ module RubyLsp
37
37
  @document.parse_result.value,
38
38
  char_position,
39
39
  node_types: [Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode],
40
+ encoding: @global_state.encoding,
40
41
  )
41
42
  target = node_context.node
42
43
  parent = node_context.parent
@@ -66,7 +67,8 @@ module RubyLsp
66
67
  end
67
68
 
68
69
  fully_qualified_name = T.must(entries.first).name
69
- changes = collect_text_edits(fully_qualified_name, name)
70
+ reference_target = RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name)
71
+ changes = collect_text_edits(reference_target, name)
70
72
 
71
73
  # If the client doesn't support resource operations, such as renaming files, then we can only return the basic
72
74
  # text changes
@@ -127,8 +129,13 @@ module RubyLsp
127
129
  end
128
130
  end
129
131
 
130
- sig { params(fully_qualified_name: String, name: String).returns(T::Hash[String, T::Array[Interface::TextEdit]]) }
131
- def collect_text_edits(fully_qualified_name, name)
132
+ sig do
133
+ params(
134
+ target: RubyIndexer::ReferenceFinder::Target,
135
+ name: String,
136
+ ).returns(T::Hash[String, T::Array[Interface::TextEdit]])
137
+ end
138
+ def collect_text_edits(target, name)
132
139
  changes = {}
133
140
 
134
141
  Dir.glob(File.join(@global_state.workspace_path, "**/*.rb")).each do |path|
@@ -138,12 +145,12 @@ module RubyLsp
138
145
  next if @store.key?(uri)
139
146
 
140
147
  parse_result = Prism.parse_file(path)
141
- edits = collect_changes(fully_qualified_name, parse_result, name, uri)
148
+ edits = collect_changes(target, parse_result, name, uri)
142
149
  changes[uri.to_s] = edits unless edits.empty?
143
150
  end
144
151
 
145
152
  @store.each do |uri, document|
146
- edits = collect_changes(fully_qualified_name, document.parse_result, name, document.uri)
153
+ edits = collect_changes(target, document.parse_result, name, document.uri)
147
154
  changes[uri] = edits unless edits.empty?
148
155
  end
149
156
 
@@ -152,18 +159,18 @@ module RubyLsp
152
159
 
153
160
  sig do
154
161
  params(
155
- fully_qualified_name: String,
162
+ target: RubyIndexer::ReferenceFinder::Target,
156
163
  parse_result: Prism::ParseResult,
157
164
  name: String,
158
165
  uri: URI::Generic,
159
166
  ).returns(T::Array[Interface::TextEdit])
160
167
  end
161
- def collect_changes(fully_qualified_name, parse_result, name, uri)
168
+ def collect_changes(target, parse_result, name, uri)
162
169
  dispatcher = Prism::Dispatcher.new
163
- finder = RubyIndexer::ReferenceFinder.new(fully_qualified_name, @global_state.index, dispatcher)
170
+ finder = RubyIndexer::ReferenceFinder.new(target, @global_state.index, dispatcher)
164
171
  dispatcher.visit(parse_result.value)
165
172
 
166
- finder.references.uniq(&:location).map do |reference|
173
+ finder.references.map do |reference|
167
174
  adjust_reference_for_edit(name, reference)
168
175
  end
169
176
  end
@@ -39,7 +39,12 @@ module RubyLsp
39
39
  char_position = document.create_scanner.find_char_position(position)
40
40
  delegate_request_if_needed!(global_state, document, char_position)
41
41
 
42
- node_context = RubyDocument.locate(document.parse_result.value, char_position, node_types: [Prism::CallNode])
42
+ node_context = RubyDocument.locate(
43
+ document.parse_result.value,
44
+ char_position,
45
+ node_types: [Prism::CallNode],
46
+ encoding: global_state.encoding,
47
+ )
43
48
 
44
49
  target = adjust_for_nested_target(node_context.node, node_context.parent, position)
45
50
 
@@ -13,6 +13,9 @@ module RubyLsp
13
13
  sig { abstract.params(uri: URI::Generic, document: RubyDocument).returns(T.nilable(String)) }
14
14
  def run_formatting(uri, document); end
15
15
 
16
+ sig { abstract.params(uri: URI::Generic, source: String, base_indentation: Integer).returns(T.nilable(String)) }
17
+ def run_range_formatting(uri, source, base_indentation); end
18
+
16
19
  sig do
17
20
  abstract.params(
18
21
  uri: URI::Generic,
@@ -28,6 +28,12 @@ module RubyLsp
28
28
  @format_runner.formatted_source
29
29
  end
30
30
 
31
+ # RuboCop does not support range formatting
32
+ sig { override.params(uri: URI::Generic, source: String, base_indentation: Integer).returns(T.nilable(String)) }
33
+ def run_range_formatting(uri, source, base_indentation)
34
+ nil
35
+ end
36
+
31
37
  sig do
32
38
  override.params(
33
39
  uri: URI::Generic,
@@ -37,6 +37,14 @@ module RubyLsp
37
37
  SyntaxTree.format(document.source, @options.print_width, options: @options.formatter_options)
38
38
  end
39
39
 
40
+ sig { override.params(uri: URI::Generic, source: String, base_indentation: Integer).returns(T.nilable(String)) }
41
+ def run_range_formatting(uri, source, base_indentation)
42
+ path = uri.to_standardized_path
43
+ return if path && @options.ignore_files.any? { |pattern| File.fnmatch?("*/#{pattern}", path) }
44
+
45
+ SyntaxTree.format(source, @options.print_width, base_indentation, options: @options.formatter_options)
46
+ end
47
+
40
48
  sig do
41
49
  override.params(
42
50
  uri: URI::Generic,
@@ -26,9 +26,10 @@ module RubyLsp
26
26
  node: Prism::Node,
27
27
  char_position: Integer,
28
28
  node_types: T::Array[T.class_of(Prism::Node)],
29
+ encoding: Encoding,
29
30
  ).returns(NodeContext)
30
31
  end
31
- def locate(node, char_position, node_types: [])
32
+ def locate(node, char_position, node_types: [], encoding: Encoding::UTF_8)
32
33
  queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
33
34
  closest = node
34
35
  parent = T.let(nil, T.nilable(Prism::Node))
@@ -61,16 +62,21 @@ module RubyLsp
61
62
 
62
63
  # Skip if the current node doesn't cover the desired position
63
64
  loc = candidate.location
64
- next unless (loc.start_offset...loc.end_offset).cover?(char_position)
65
+ loc_start_offset = loc.start_code_units_offset(encoding)
66
+ loc_end_offset = loc.end_code_units_offset(encoding)
67
+ next unless (loc_start_offset...loc_end_offset).cover?(char_position)
65
68
 
66
69
  # If the node's start character is already past the position, then we should've found the closest node
67
70
  # already
68
- break if char_position < loc.start_offset
71
+ break if char_position < loc_start_offset
69
72
 
70
73
  # If the candidate starts after the end of the previous nesting level, then we've exited that nesting level
71
74
  # and need to pop the stack
72
75
  previous_level = nesting_nodes.last
73
- nesting_nodes.pop if previous_level && loc.start_offset > previous_level.location.end_offset
76
+ if previous_level &&
77
+ (loc_start_offset > previous_level.location.end_code_units_offset(encoding))
78
+ nesting_nodes.pop
79
+ end
74
80
 
75
81
  # Keep track of the nesting where we found the target. This is used to determine the fully qualified name of
76
82
  # the target when it is a constant
@@ -83,8 +89,10 @@ module RubyLsp
83
89
  if candidate.is_a?(Prism::CallNode)
84
90
  arg_loc = candidate.arguments&.location
85
91
  blk_loc = candidate.block&.location
86
- if (arg_loc && (arg_loc.start_offset...arg_loc.end_offset).cover?(char_position)) ||
87
- (blk_loc && (blk_loc.start_offset...blk_loc.end_offset).cover?(char_position))
92
+ if (arg_loc && (arg_loc.start_code_units_offset(encoding)...
93
+ arg_loc.end_code_units_offset(encoding)).cover?(char_position)) ||
94
+ (blk_loc && (blk_loc.start_code_units_offset(encoding)...
95
+ blk_loc.end_code_units_offset(encoding)).cover?(char_position))
88
96
  call_node = candidate
89
97
  end
90
98
  end
@@ -94,7 +102,9 @@ module RubyLsp
94
102
 
95
103
  # If the current node is narrower than or equal to the previous closest node, then it is more precise
96
104
  closest_loc = closest.location
97
- if loc.end_offset - loc.start_offset <= closest_loc.end_offset - closest_loc.start_offset
105
+ closest_node_start_offset = closest_loc.start_code_units_offset(encoding)
106
+ closest_node_end_offset = closest_loc.end_code_units_offset(encoding)
107
+ if loc_end_offset - loc_start_offset <= closest_node_end_offset - closest_node_start_offset
98
108
  parent = closest
99
109
  closest = candidate
100
110
  end
@@ -201,7 +211,12 @@ module RubyLsp
201
211
  ).returns(NodeContext)
202
212
  end
203
213
  def locate_node(position, node_types: [])
204
- RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
214
+ RubyDocument.locate(
215
+ @parse_result.value,
216
+ create_scanner.find_char_position(position),
217
+ node_types: node_types,
218
+ encoding: @encoding,
219
+ )
205
220
  end
206
221
  end
207
222
  end
@@ -43,6 +43,8 @@ module RubyLsp
43
43
  text_document_semantic_tokens_range(message)
44
44
  when "textDocument/formatting"
45
45
  text_document_formatting(message)
46
+ when "textDocument/rangeFormatting"
47
+ text_document_range_formatting(message)
46
48
  when "textDocument/documentHighlight"
47
49
  text_document_document_highlight(message)
48
50
  when "textDocument/onTypeFormatting"
@@ -69,6 +71,8 @@ module RubyLsp
69
71
  text_document_prepare_type_hierarchy(message)
70
72
  when "textDocument/rename"
71
73
  text_document_rename(message)
74
+ when "textDocument/references"
75
+ text_document_references(message)
72
76
  when "typeHierarchy/supertypes"
73
77
  type_hierarchy_supertypes(message)
74
78
  when "typeHierarchy/subtypes"
@@ -87,7 +91,16 @@ module RubyLsp
87
91
  id: message[:id],
88
92
  response:
89
93
  Addon.addons.map do |addon|
90
- { name: addon.name, errored: addon.error? }
94
+ version_method = addon.method(:version)
95
+
96
+ # If the add-on doesn't define a `version` method, we'd be calling the abstract method defined by
97
+ # Sorbet, which would raise an error.
98
+ # Therefore, we only call the method if it's defined by the add-on itself
99
+ if version_method.owner != Addon
100
+ version = addon.version
101
+ end
102
+
103
+ { name: addon.name, version: version, errored: addon.error? }
91
104
  end,
92
105
  ),
93
106
  )
@@ -230,6 +243,8 @@ module RubyLsp
230
243
  signature_help_provider: signature_help_provider,
231
244
  type_hierarchy_provider: type_hierarchy_provider,
232
245
  rename_provider: !@global_state.has_type_checker,
246
+ references_provider: !@global_state.has_type_checker,
247
+ document_range_formatting_provider: true,
233
248
  experimental: {
234
249
  addon_detection: true,
235
250
  },
@@ -513,6 +528,34 @@ module RubyLsp
513
528
  send_message(Result.new(id: message[:id], response: request.perform))
514
529
  end
515
530
 
531
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
532
+ def text_document_range_formatting(message)
533
+ # If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
534
+ if @global_state.formatter == "none"
535
+ send_empty_response(message[:id])
536
+ return
537
+ end
538
+
539
+ params = message[:params]
540
+ uri = params.dig(:textDocument, :uri)
541
+ # Do not format files outside of the workspace. For example, if someone is looking at a gem's source code, we
542
+ # don't want to format it
543
+ path = uri.to_standardized_path
544
+ unless path.nil? || path.start_with?(@global_state.workspace_path)
545
+ send_empty_response(message[:id])
546
+ return
547
+ end
548
+
549
+ document = @store.get(uri)
550
+ unless document.is_a?(RubyDocument)
551
+ send_empty_response(message[:id])
552
+ return
553
+ end
554
+
555
+ response = Requests::RangeFormatting.new(@global_state, document, params).perform
556
+ send_message(Result.new(id: message[:id], response: response))
557
+ end
558
+
516
559
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
517
560
  def text_document_formatting(message)
518
561
  # If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
@@ -636,6 +679,24 @@ module RubyLsp
636
679
  send_message(Error.new(id: message[:id], code: Constant::ErrorCodes::REQUEST_FAILED, message: e.message))
637
680
  end
638
681
 
682
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
683
+ def text_document_references(message)
684
+ params = message[:params]
685
+ document = @store.get(params.dig(:textDocument, :uri))
686
+
687
+ unless document.is_a?(RubyDocument)
688
+ send_empty_response(message[:id])
689
+ return
690
+ end
691
+
692
+ send_message(
693
+ Result.new(
694
+ id: message[:id],
695
+ response: Requests::References.new(@global_state, @store, document, params).perform,
696
+ ),
697
+ )
698
+ end
699
+
639
700
  sig { params(document: Document[T.untyped]).returns(RubyDocument::SorbetLevel) }
640
701
  def sorbet_level(document)
641
702
  return RubyDocument::SorbetLevel::Ignore unless @global_state.has_type_checker
@@ -711,7 +772,7 @@ module RubyLsp
711
772
  return
712
773
  end
713
774
 
714
- result = Requests::CodeActionResolve.new(document, params).perform
775
+ result = Requests::CodeActionResolve.new(document, @global_state, params).perform
715
776
 
716
777
  case result
717
778
  when Requests::CodeActionResolve::Error::EmptySelection
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.0
4
+ version: 0.19.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-02 00:00:00.000000000 Z
11
+ date: 2024-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -154,6 +154,8 @@ files:
154
154
  - lib/ruby_lsp/requests/inlay_hints.rb
155
155
  - lib/ruby_lsp/requests/on_type_formatting.rb
156
156
  - lib/ruby_lsp/requests/prepare_type_hierarchy.rb
157
+ - lib/ruby_lsp/requests/range_formatting.rb
158
+ - lib/ruby_lsp/requests/references.rb
157
159
  - lib/ruby_lsp/requests/rename.rb
158
160
  - lib/ruby_lsp/requests/request.rb
159
161
  - lib/ruby_lsp/requests/selection_ranges.rb