ruby-lsp 0.19.0 → 0.19.1

Sign up to get free protection for your applications and to get access to all the features.
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