ruby-lsp 0.17.10 → 0.17.12

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: 7fafb2e9f1ef8ae394523d09256bce9016307821ab32ecd17fe645dd3f9d88f1
4
- data.tar.gz: 74396754fdbf6dffb91ff4d6d237f67c26b92fd7ec0f187fc3e40627dc214de1
3
+ metadata.gz: 34cae8b53cfb0811cb26d3b3dd4198a72585d1f15a98a7d8f656fd1676886630
4
+ data.tar.gz: f4868222c46c4cbad9a099fce69562838b5dda567f1666a82752f45fe3f501c5
5
5
  SHA512:
6
- metadata.gz: 8901ced915cabc4e749cc9451de7d7f9927c4025c6484002a682e65b1a20f7e52d38d3e5b52255b061998e8c114da8efeaaf605f0419fff8ed93c09cf00a2dae
7
- data.tar.gz: 5679783fa6a828a7e6a3db9fb193ec9b4c1c094c6a67c187f7c2169f5f11fe53fe5024b0c7b0f08273eb46293639ff1d67f7787a851f4f1ac599be23a10ff7dd
6
+ metadata.gz: db8e991497aa6e9388b2778fb9a417adbef07c6bb29165ff9a1718ab7cf1db6d2a6586453320d72adf0cffe78b846b553c3a0ba24b63807c52170f02f1b7d2e3
7
+ data.tar.gz: 80aac045ca6aaf7dbb9344f947a9b1419eaac4711448d997c902f0fb9da68e7c2338e0a5b1d81a30c0a2609defe1b656457d1a4a23b0ba5288162d142cf7c9e9
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.10
1
+ 0.17.12
@@ -8,12 +8,22 @@ module RubyIndexer
8
8
  OBJECT_NESTING = T.let(["Object"].freeze, T::Array[String])
9
9
  BASIC_OBJECT_NESTING = T.let(["BasicObject"].freeze, T::Array[String])
10
10
 
11
+ sig { returns(T::Array[String]) }
12
+ attr_reader :indexing_errors
13
+
11
14
  sig do
12
- params(index: Index, dispatcher: Prism::Dispatcher, parse_result: Prism::ParseResult, file_path: String).void
15
+ params(
16
+ index: Index,
17
+ dispatcher: Prism::Dispatcher,
18
+ parse_result: Prism::ParseResult,
19
+ file_path: String,
20
+ enhancements: T::Array[Enhancement],
21
+ ).void
13
22
  end
14
- def initialize(index, dispatcher, parse_result, file_path)
23
+ def initialize(index, dispatcher, parse_result, file_path, enhancements: [])
15
24
  @index = index
16
25
  @file_path = file_path
26
+ @enhancements = enhancements
17
27
  @visibility_stack = T.let([Entry::Visibility::PUBLIC], T::Array[Entry::Visibility])
18
28
  @comments_by_line = T.let(
19
29
  parse_result.comments.to_h do |c|
@@ -29,6 +39,7 @@ module RubyIndexer
29
39
 
30
40
  # A stack of namespace entries that represent where we currently are. Used to properly assign methods to an owner
31
41
  @owner_stack = T.let([], T::Array[Entry::Namespace])
42
+ @indexing_errors = T.let([], T::Array[String])
32
43
 
33
44
  dispatcher.register(
34
45
  self,
@@ -279,6 +290,12 @@ module RubyIndexer
279
290
  when :private
280
291
  @visibility_stack.push(Entry::Visibility::PRIVATE)
281
292
  end
293
+
294
+ @enhancements.each do |enhancement|
295
+ enhancement.on_call_node(@index, @owner_stack.last, node, @file_path)
296
+ rescue StandardError => e
297
+ @indexing_errors << "Indexing error in #{@file_path} with '#{enhancement.class.name}' enhancement: #{e.message}"
298
+ end
282
299
  end
283
300
 
284
301
  sig { params(node: Prism::CallNode).void }
@@ -0,0 +1,26 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyIndexer
5
+ module Enhancement
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ interface!
10
+
11
+ requires_ancestor { Object }
12
+
13
+ # The `on_extend` indexing enhancement is invoked whenever an extend is encountered in the code. It can be used to
14
+ # register for an included callback, similar to what `ActiveSupport::Concern` does in order to auto-extend the
15
+ # `ClassMethods` modules
16
+ sig do
17
+ abstract.params(
18
+ index: Index,
19
+ owner: T.nilable(Entry::Namespace),
20
+ node: Prism::CallNode,
21
+ file_path: String,
22
+ ).void
23
+ end
24
+ def on_call_node(index, owner, node, file_path); end
25
+ end
26
+ end
@@ -342,6 +342,19 @@ module RubyIndexer
342
342
 
343
343
  "(#{first_signature.format})"
344
344
  end
345
+
346
+ sig { returns(String) }
347
+ def formatted_signatures
348
+ overloads_count = signatures.size
349
+ case overloads_count
350
+ when 1
351
+ ""
352
+ when 2
353
+ "\n(+1 overload)"
354
+ else
355
+ "\n(+#{overloads_count - 1} overloads)"
356
+ end
357
+ end
345
358
  end
346
359
 
347
360
  class Accessor < Member
@@ -542,6 +555,16 @@ module RubyIndexer
542
555
  def decorated_parameters
543
556
  @target.decorated_parameters
544
557
  end
558
+
559
+ sig { returns(String) }
560
+ def formatted_signatures
561
+ @target.formatted_signatures
562
+ end
563
+
564
+ sig { returns(T::Array[Signature]) }
565
+ def signatures
566
+ @target.signatures
567
+ end
545
568
  end
546
569
 
547
570
  # Ruby doesn't support method overloading, so a method will have only one signature.
@@ -35,6 +35,27 @@ module RubyIndexer
35
35
 
36
36
  # Holds the linearized ancestors list for every namespace
37
37
  @ancestors = T.let({}, T::Hash[String, T::Array[String]])
38
+
39
+ # List of classes that are enhancing the index
40
+ @enhancements = T.let([], T::Array[Enhancement])
41
+
42
+ # Map of module name to included hooks that have to be executed when we include the given module
43
+ @included_hooks = T.let(
44
+ {},
45
+ T::Hash[String, T::Array[T.proc.params(index: Index, base: Entry::Namespace).void]],
46
+ )
47
+ end
48
+
49
+ # Register an enhancement to the index. Enhancements must conform to the `Enhancement` interface
50
+ sig { params(enhancement: Enhancement).void }
51
+ def register_enhancement(enhancement)
52
+ @enhancements << enhancement
53
+ end
54
+
55
+ # Register an included `hook` that will be executed when `module_name` is included into any namespace
56
+ sig { params(module_name: String, hook: T.proc.params(index: Index, base: Entry::Namespace).void).void }
57
+ def register_included_hook(module_name, &hook)
58
+ (@included_hooks[module_name] ||= []) << hook
38
59
  end
39
60
 
40
61
  sig { params(indexable: IndexablePath).void }
@@ -296,11 +317,25 @@ module RubyIndexer
296
317
  dispatcher = Prism::Dispatcher.new
297
318
 
298
319
  result = Prism.parse(content)
299
- DeclarationListener.new(self, dispatcher, result, indexable_path.full_path)
320
+ listener = DeclarationListener.new(
321
+ self,
322
+ dispatcher,
323
+ result,
324
+ indexable_path.full_path,
325
+ enhancements: @enhancements,
326
+ )
300
327
  dispatcher.dispatch(result.value)
301
328
 
329
+ indexing_errors = listener.indexing_errors.uniq
330
+
302
331
  require_path = indexable_path.require_path
303
332
  @require_paths_tree.insert(require_path, indexable_path) if require_path
333
+
334
+ if indexing_errors.any?
335
+ indexing_errors.each do |error|
336
+ $stderr.puts error
337
+ end
338
+ end
304
339
  rescue Errno::EISDIR, Errno::ENOENT
305
340
  # If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
306
341
  # it
@@ -457,6 +492,12 @@ module RubyIndexer
457
492
  end
458
493
  end
459
494
 
495
+ # We only need to run included hooks when linearizing singleton classes. Included hooks are typically used to add
496
+ # new singleton methods or to extend a module through an include. There's no need to support instance methods, the
497
+ # inclusion of another module or the prepending of another module, because those features are already a part of
498
+ # Ruby and can be used directly without any metaprogramming
499
+ run_included_hooks(attached_class_name, nesting) if singleton_levels > 0
500
+
460
501
  linearize_mixins(ancestors, namespaces, nesting)
461
502
  linearize_superclass(
462
503
  ancestors,
@@ -570,6 +611,34 @@ module RubyIndexer
570
611
 
571
612
  private
572
613
 
614
+ # Runs the registered included hooks
615
+ sig { params(fully_qualified_name: String, nesting: T::Array[String]).void }
616
+ def run_included_hooks(fully_qualified_name, nesting)
617
+ return if @included_hooks.empty?
618
+
619
+ namespaces = self[fully_qualified_name]&.grep(Entry::Namespace)
620
+ return unless namespaces
621
+
622
+ namespaces.each do |namespace|
623
+ namespace.mixin_operations.each do |operation|
624
+ next unless operation.is_a?(Entry::Include)
625
+
626
+ # First we resolve the include name, so that we know the actual module being referred to in the include
627
+ resolved_modules = resolve(operation.module_name, nesting)
628
+ next unless resolved_modules
629
+
630
+ module_name = T.must(resolved_modules.first).name
631
+
632
+ # Then we grab any hooks registered for that module
633
+ hooks = @included_hooks[module_name]
634
+ next unless hooks
635
+
636
+ # We invoke the hooks with the index and the namespace that included the module
637
+ hooks.each { |hook| hook.call(self, namespace) }
638
+ end
639
+ end
640
+ end
641
+
573
642
  # Linearize mixins for an array of namespace entries. This method will mutate the `ancestors` array with the
574
643
  # linearized ancestors of the mixins
575
644
  sig do
@@ -6,6 +6,7 @@ require "did_you_mean"
6
6
 
7
7
  require "ruby_indexer/lib/ruby_indexer/indexable_path"
8
8
  require "ruby_indexer/lib/ruby_indexer/declaration_listener"
9
+ require "ruby_indexer/lib/ruby_indexer/enhancement"
9
10
  require "ruby_indexer/lib/ruby_indexer/index"
10
11
  require "ruby_indexer/lib/ruby_indexer/entry"
11
12
  require "ruby_indexer/lib/ruby_indexer/configuration"
@@ -0,0 +1,197 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "test_case"
5
+
6
+ module RubyIndexer
7
+ class EnhancementTest < TestCase
8
+ def test_enhancing_indexing_included_hook
9
+ enhancement_class = Class.new do
10
+ include Enhancement
11
+
12
+ def on_call_node(index, owner, node, file_path)
13
+ return unless owner
14
+ return unless node.name == :extend
15
+
16
+ arguments = node.arguments&.arguments
17
+ return unless arguments
18
+
19
+ location = node.location
20
+
21
+ arguments.each do |node|
22
+ next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
23
+
24
+ module_name = node.full_name
25
+ next unless module_name == "ActiveSupport::Concern"
26
+
27
+ index.register_included_hook(owner.name) do |index, base|
28
+ class_methods_name = "#{owner.name}::ClassMethods"
29
+
30
+ if index.indexed?(class_methods_name)
31
+ singleton = index.existing_or_new_singleton_class(base.name)
32
+ singleton.mixin_operations << Entry::Include.new(class_methods_name)
33
+ end
34
+ end
35
+
36
+ index.add(Entry::Method.new(
37
+ "new_method",
38
+ file_path,
39
+ location,
40
+ location,
41
+ [],
42
+ [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])],
43
+ Entry::Visibility::PUBLIC,
44
+ owner,
45
+ ))
46
+ rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
47
+ Prism::ConstantPathNode::MissingNodesInConstantPathError
48
+ # Do nothing
49
+ end
50
+ end
51
+ end
52
+
53
+ @index.register_enhancement(enhancement_class.new)
54
+ index(<<~RUBY)
55
+ module ActiveSupport
56
+ module Concern
57
+ def self.extended(base)
58
+ base.class_eval("def new_method(a); end")
59
+ end
60
+ end
61
+ end
62
+
63
+ module ActiveRecord
64
+ module Associations
65
+ extend ActiveSupport::Concern
66
+
67
+ module ClassMethods
68
+ def belongs_to(something); end
69
+ end
70
+ end
71
+
72
+ class Base
73
+ include Associations
74
+ end
75
+ end
76
+
77
+ class User < ActiveRecord::Base
78
+ end
79
+ RUBY
80
+
81
+ assert_equal(
82
+ [
83
+ "User::<Class:User>",
84
+ "ActiveRecord::Base::<Class:Base>",
85
+ "ActiveRecord::Associations::ClassMethods",
86
+ "Object::<Class:Object>",
87
+ "BasicObject::<Class:BasicObject>",
88
+ "Class",
89
+ "Module",
90
+ "Object",
91
+ "Kernel",
92
+ "BasicObject",
93
+ ],
94
+ @index.linearized_ancestors_of("User::<Class:User>"),
95
+ )
96
+
97
+ assert_entry("new_method", Entry::Method, "/fake/path/foo.rb:10-4:10-33")
98
+ end
99
+
100
+ def test_enhancing_indexing_configuration_dsl
101
+ enhancement_class = Class.new do
102
+ include Enhancement
103
+
104
+ def on_call_node(index, owner, node, file_path)
105
+ return unless owner
106
+
107
+ name = node.name
108
+ return unless name == :has_many
109
+
110
+ arguments = node.arguments&.arguments
111
+ return unless arguments
112
+
113
+ association_name = arguments.first
114
+ return unless association_name.is_a?(Prism::SymbolNode)
115
+
116
+ location = association_name.location
117
+
118
+ index.add(Entry::Method.new(
119
+ T.must(association_name.value),
120
+ file_path,
121
+ location,
122
+ location,
123
+ [],
124
+ [],
125
+ Entry::Visibility::PUBLIC,
126
+ owner,
127
+ ))
128
+ end
129
+ end
130
+
131
+ @index.register_enhancement(enhancement_class.new)
132
+ index(<<~RUBY)
133
+ module ActiveSupport
134
+ module Concern
135
+ def self.extended(base)
136
+ base.class_eval("def new_method(a); end")
137
+ end
138
+ end
139
+ end
140
+
141
+ module ActiveRecord
142
+ module Associations
143
+ extend ActiveSupport::Concern
144
+
145
+ module ClassMethods
146
+ def belongs_to(something); end
147
+ end
148
+ end
149
+
150
+ class Base
151
+ include Associations
152
+ end
153
+ end
154
+
155
+ class User < ActiveRecord::Base
156
+ has_many :posts
157
+ end
158
+ RUBY
159
+
160
+ assert_entry("posts", Entry::Method, "/fake/path/foo.rb:23-11:23-17")
161
+ end
162
+
163
+ def test_error_handling_in_enhancement
164
+ enhancement_class = Class.new do
165
+ include Enhancement
166
+
167
+ def on_call_node(index, owner, node, file_path)
168
+ raise "Error"
169
+ end
170
+
171
+ class << self
172
+ def name
173
+ "TestEnhancement"
174
+ end
175
+ end
176
+ end
177
+
178
+ @index.register_enhancement(enhancement_class.new)
179
+
180
+ _stdout, stderr = capture_io do
181
+ index(<<~RUBY)
182
+ module ActiveSupport
183
+ module Concern
184
+ def self.extended(base)
185
+ base.class_eval("def new_method(a); end")
186
+ end
187
+ end
188
+ end
189
+ RUBY
190
+ end
191
+
192
+ assert_match(%r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' enhancement}, stderr)
193
+ # The module should still be indexed
194
+ assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5")
195
+ end
196
+ end
197
+ end
@@ -223,6 +223,40 @@ module RubyLsp
223
223
  NodeContext.new(closest, parent, nesting_nodes, call_node)
224
224
  end
225
225
 
226
+ sig do
227
+ params(
228
+ range: T::Hash[Symbol, T.untyped],
229
+ node_types: T::Array[T.class_of(Prism::Node)],
230
+ ).returns(T.nilable(Prism::Node))
231
+ end
232
+ def locate_first_within_range(range, node_types: [])
233
+ scanner = create_scanner
234
+ start_position = scanner.find_char_position(range[:start])
235
+ end_position = scanner.find_char_position(range[:end])
236
+ desired_range = (start_position...end_position)
237
+ queue = T.let(@parse_result.value.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
238
+
239
+ until queue.empty?
240
+ candidate = queue.shift
241
+
242
+ # Skip nil child nodes
243
+ next if candidate.nil?
244
+
245
+ # Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the
246
+ # same order as the visiting mechanism, which means searching the child nodes before moving on to the next
247
+ # sibling
248
+ T.unsafe(queue).unshift(*candidate.child_nodes)
249
+
250
+ # Skip if the current node doesn't cover the desired position
251
+ loc = candidate.location
252
+
253
+ if desired_range.cover?(loc.start_offset...loc.end_offset) &&
254
+ (node_types.empty? || node_types.any? { |type| candidate.class == type })
255
+ return candidate
256
+ end
257
+ end
258
+ end
259
+
226
260
  sig { returns(SorbetLevel) }
227
261
  def sorbet_level
228
262
  sigil = parse_result.magic_comments.find do |comment|
@@ -46,6 +46,7 @@ module RubyLsp
46
46
  :on_instance_variable_or_write_node_enter,
47
47
  :on_instance_variable_target_node_enter,
48
48
  :on_string_node_enter,
49
+ :on_symbol_node_enter,
49
50
  :on_super_node_enter,
50
51
  :on_forwarding_super_node_enter,
51
52
  )
@@ -54,9 +55,7 @@ module RubyLsp
54
55
  sig { params(node: Prism::CallNode).void }
55
56
  def on_call_node_enter(node)
56
57
  # Sorbet can handle go to definition for methods invoked on self on typed true or higher
57
- if (@sorbet_level == Document::SorbetLevel::True || @sorbet_level == Document::SorbetLevel::Strict) &&
58
- self_receiver?(node)
59
- end
58
+ return if sorbet_level_true_or_higher?(@sorbet_level) && self_receiver?(node)
60
59
 
61
60
  message = node.message
62
61
  return unless message
@@ -83,6 +82,17 @@ module RubyLsp
83
82
  handle_require_definition(node, name)
84
83
  end
85
84
 
85
+ sig { params(node: Prism::SymbolNode).void }
86
+ def on_symbol_node_enter(node)
87
+ enclosing_call = @node_context.call_node
88
+ return unless enclosing_call
89
+
90
+ name = enclosing_call.name
91
+ return unless name == :autoload
92
+
93
+ handle_autoload_definition(enclosing_call)
94
+ end
95
+
86
96
  sig { params(node: Prism::BlockArgumentNode).void }
87
97
  def on_block_argument_node_enter(node)
88
98
  expression = node.expression
@@ -253,6 +263,17 @@ module RubyLsp
253
263
  end
254
264
  end
255
265
 
266
+ sig { params(node: Prism::CallNode).void }
267
+ def handle_autoload_definition(node)
268
+ argument = node.arguments&.arguments&.first
269
+ return unless argument.is_a?(Prism::SymbolNode)
270
+
271
+ constant_name = argument.value
272
+ return unless constant_name
273
+
274
+ find_in_index(constant_name)
275
+ end
276
+
256
277
  sig { params(value: String).void }
257
278
  def find_in_index(value)
258
279
  entries = @index.resolve(value, @node_context.nesting)
@@ -174,7 +174,10 @@ module RubyLsp
174
174
  methods = @index.resolve_method(message, type.name, inherited_only: inherited_only)
175
175
  return unless methods
176
176
 
177
- title = "#{message}#{T.must(methods.first).decorated_parameters}"
177
+ first_method = T.must(methods.first)
178
+
179
+ title = "#{message}#{first_method.decorated_parameters}"
180
+ title << first_method.formatted_signatures
178
181
 
179
182
  if type.is_a?(TypeInferrer::GuessedType)
180
183
  title << "\n\nGuessed receiver: #{type.name}"
@@ -23,6 +23,8 @@ module RubyLsp
23
23
  #
24
24
  class CodeActionResolve < Request
25
25
  extend T::Sig
26
+ include Support::Common
27
+
26
28
  NEW_VARIABLE_NAME = "new_variable"
27
29
  NEW_METHOD_NAME = "new_method"
28
30
 
@@ -45,20 +47,62 @@ module RubyLsp
45
47
 
46
48
  sig { override.returns(T.any(Interface::CodeAction, Error)) }
47
49
  def perform
50
+ return Error::EmptySelection if @document.source.empty?
51
+
48
52
  case @code_action[:title]
49
53
  when CodeActions::EXTRACT_TO_VARIABLE_TITLE
50
54
  refactor_variable
51
55
  when CodeActions::EXTRACT_TO_METHOD_TITLE
52
56
  refactor_method
57
+ when CodeActions::SWITCH_BLOCK_STYLE_TITLE
58
+ switch_block_style
53
59
  else
54
60
  Error::UnknownCodeAction
55
61
  end
56
62
  end
57
63
 
64
+ private
65
+
58
66
  sig { returns(T.any(Interface::CodeAction, Error)) }
59
- def refactor_variable
60
- return Error::EmptySelection if @document.source.empty?
67
+ def switch_block_style
68
+ source_range = @code_action.dig(:data, :range)
69
+ return Error::EmptySelection if source_range[:start] == source_range[:end]
70
+
71
+ target = @document.locate_first_within_range(
72
+ @code_action.dig(:data, :range),
73
+ node_types: [Prism::CallNode],
74
+ )
75
+
76
+ return Error::InvalidTargetRange unless target.is_a?(Prism::CallNode)
77
+
78
+ node = target.block
79
+ return Error::InvalidTargetRange unless node.is_a?(Prism::BlockNode)
61
80
 
81
+ indentation = " " * target.location.start_column unless node.opening_loc.slice == "do"
82
+
83
+ Interface::CodeAction.new(
84
+ title: CodeActions::SWITCH_BLOCK_STYLE_TITLE,
85
+ edit: Interface::WorkspaceEdit.new(
86
+ document_changes: [
87
+ Interface::TextDocumentEdit.new(
88
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
89
+ uri: @code_action.dig(:data, :uri),
90
+ version: nil,
91
+ ),
92
+ edits: [
93
+ Interface::TextEdit.new(
94
+ range: range_from_location(node.location),
95
+ new_text: recursively_switch_nested_block_styles(node, indentation),
96
+ ),
97
+ ],
98
+ ),
99
+ ],
100
+ ),
101
+ )
102
+ end
103
+
104
+ sig { returns(T.any(Interface::CodeAction, Error)) }
105
+ def refactor_variable
62
106
  source_range = @code_action.dig(:data, :range)
63
107
  return Error::EmptySelection if source_range[:start] == source_range[:end]
64
108
 
@@ -153,8 +197,6 @@ module RubyLsp
153
197
 
154
198
  sig { returns(T.any(Interface::CodeAction, Error)) }
155
199
  def refactor_method
156
- return Error::EmptySelection if @document.source.empty?
157
-
158
200
  source_range = @code_action.dig(:data, :range)
159
201
  return Error::EmptySelection if source_range[:start] == source_range[:end]
160
202
 
@@ -206,8 +248,6 @@ module RubyLsp
206
248
  )
207
249
  end
208
250
 
209
- private
210
-
211
251
  sig { params(range: T::Hash[Symbol, T.untyped], new_text: String).returns(Interface::TextEdit) }
212
252
  def create_text_edit(range, new_text)
213
253
  Interface::TextEdit.new(
@@ -218,6 +258,64 @@ module RubyLsp
218
258
  new_text: new_text,
219
259
  )
220
260
  end
261
+
262
+ sig { params(node: Prism::BlockNode, indentation: T.nilable(String)).returns(String) }
263
+ def recursively_switch_nested_block_styles(node, indentation)
264
+ parameters = node.parameters
265
+ body = node.body
266
+
267
+ # We use the indentation to differentiate between do...end and brace style blocks because only the do...end
268
+ # style requires the indentation to build the edit.
269
+ #
270
+ # If the block is using `do...end` style, we change it to a single line brace block. Newlines are turned into
271
+ # semi colons, so that the result is valid Ruby code and still a one liner. If the block is using brace style,
272
+ # we do the opposite and turn it into a `do...end` block, making all semi colons into newlines.
273
+ source = +""
274
+
275
+ if indentation
276
+ source << "do"
277
+ source << " #{parameters.slice}" if parameters
278
+ source << "\n#{indentation} "
279
+ source << switch_block_body(body, indentation) if body
280
+ source << "\n#{indentation}end"
281
+ else
282
+ source << "{ "
283
+ source << "#{parameters.slice} " if parameters
284
+ source << switch_block_body(body, nil) if body
285
+ source << "}"
286
+ end
287
+
288
+ source
289
+ end
290
+
291
+ sig { params(body: Prism::Node, indentation: T.nilable(String)).returns(String) }
292
+ def switch_block_body(body, indentation)
293
+ # Check if there are any nested blocks inside of the current block
294
+ body_loc = body.location
295
+ nested_block = @document.locate_first_within_range(
296
+ {
297
+ start: { line: body_loc.start_line - 1, character: body_loc.start_column },
298
+ end: { line: body_loc.end_line - 1, character: body_loc.end_column },
299
+ },
300
+ node_types: [Prism::BlockNode],
301
+ )
302
+
303
+ body_content = body.slice.dup
304
+
305
+ # If there are nested blocks, then we change their style too and we have to mutate the string using the
306
+ # relative position in respect to the beginning of the body
307
+ if nested_block.is_a?(Prism::BlockNode)
308
+ location = nested_block.location
309
+ correction_start = location.start_offset - body_loc.start_offset
310
+ correction_end = location.end_offset - body_loc.start_offset
311
+ next_indentation = indentation ? "#{indentation} " : nil
312
+
313
+ body_content[correction_start...correction_end] =
314
+ recursively_switch_nested_block_styles(nested_block, next_indentation)
315
+ end
316
+
317
+ indentation ? body_content.gsub(";", "\n") : "#{body_content.gsub("\n", ";")} "
318
+ end
221
319
  end
222
320
  end
223
321
  end
@@ -21,13 +21,17 @@ module RubyLsp
21
21
 
22
22
  EXTRACT_TO_VARIABLE_TITLE = "Refactor: Extract Variable"
23
23
  EXTRACT_TO_METHOD_TITLE = "Refactor: Extract Method"
24
+ SWITCH_BLOCK_STYLE_TITLE = "Refactor: Switch block style"
24
25
 
25
26
  class << self
26
27
  extend T::Sig
27
28
 
28
- sig { returns(Interface::CodeActionOptions) }
29
+ sig { returns(Interface::CodeActionRegistrationOptions) }
29
30
  def provider
30
- Interface::CodeActionOptions.new(resolve_provider: true)
31
+ Interface::CodeActionRegistrationOptions.new(
32
+ document_selector: [Interface::DocumentFilter.new(language: "ruby")],
33
+ resolve_provider: true,
34
+ )
31
35
  end
32
36
  end
33
37
 
@@ -66,6 +70,11 @@ module RubyLsp
66
70
  kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
67
71
  data: { range: @range, uri: @uri.to_s },
68
72
  )
73
+ code_actions << Interface::CodeAction.new(
74
+ title: SWITCH_BLOCK_STYLE_TITLE,
75
+ kind: Constant::CodeActionKind::REFACTOR_REWRITE,
76
+ data: { range: @range, uri: @uri.to_s },
77
+ )
69
78
  end
70
79
 
71
80
  code_actions
@@ -63,6 +63,7 @@ module RubyLsp
63
63
 
64
64
  if first_entry.is_a?(RubyIndexer::Entry::Member)
65
65
  label = +"#{label}#{first_entry.decorated_parameters}"
66
+ label << first_entry.formatted_signatures
66
67
  end
67
68
 
68
69
  guessed_type = @item.dig(:data, :guessed_type)
@@ -30,6 +30,12 @@ module RubyLsp
30
30
  extend T::Sig
31
31
  extend T::Generic
32
32
 
33
+ SPECIAL_METHOD_CALLS = [
34
+ :require,
35
+ :require_relative,
36
+ :autoload,
37
+ ].freeze
38
+
33
39
  sig do
34
40
  params(
35
41
  document: Document,
@@ -78,8 +84,9 @@ module RubyLsp
78
84
  parent,
79
85
  position,
80
86
  )
81
- elsif target.is_a?(Prism::CallNode) && target.name != :require && target.name != :require_relative &&
82
- !covers_position?(target.message_loc, position)
87
+ elsif target.is_a?(Prism::CallNode) && !SPECIAL_METHOD_CALLS.include?(target.message) && !covers_position?(
88
+ target.message_loc, position
89
+ )
83
90
  # If the target is a method call, we need to ensure that the requested position is exactly on top of the
84
91
  # method identifier. Otherwise, we risk showing definitions for unrelated things
85
92
  target = nil
@@ -22,12 +22,13 @@ module RubyLsp
22
22
  class << self
23
23
  extend T::Sig
24
24
 
25
- sig { returns(T::Hash[Symbol, T::Boolean]) }
25
+ sig { returns(Interface::DiagnosticRegistrationOptions) }
26
26
  def provider
27
- {
28
- interFileDependencies: false,
29
- workspaceDiagnostics: false,
30
- }
27
+ Interface::DiagnosticRegistrationOptions.new(
28
+ document_selector: [Interface::DocumentFilter.new(language: "ruby")],
29
+ inter_file_dependencies: false,
30
+ workspace_diagnostics: false,
31
+ )
31
32
  end
32
33
  end
33
34
 
@@ -34,14 +34,9 @@ module RubyLsp
34
34
  class << self
35
35
  extend T::Sig
36
36
 
37
- sig { returns(Interface::DocumentSymbolClientCapabilities) }
37
+ sig { returns(Interface::DocumentSymbolOptions) }
38
38
  def provider
39
- Interface::DocumentSymbolClientCapabilities.new(
40
- hierarchical_document_symbol_support: true,
41
- symbol_kind: {
42
- value_set: (Constant::SymbolKind::FILE..Constant::SymbolKind::TYPE_PARAMETER).to_a,
43
- },
44
- )
39
+ Interface::DocumentSymbolOptions.new
45
40
  end
46
41
  end
47
42
 
@@ -23,9 +23,13 @@ module RubyLsp
23
23
  class << self
24
24
  extend T::Sig
25
25
 
26
- sig { returns(Interface::FoldingRangeClientCapabilities) }
26
+ sig { returns(Interface::FoldingRangeRegistrationOptions) }
27
27
  def provider
28
- Interface::FoldingRangeClientCapabilities.new(line_folding_only: true)
28
+ Interface::FoldingRangeRegistrationOptions.new(
29
+ document_selector: [
30
+ Interface::DocumentFilter.new(language: "ruby"),
31
+ ],
32
+ )
29
33
  end
30
34
  end
31
35
 
@@ -27,6 +27,19 @@ module RubyLsp
27
27
 
28
28
  class Error < StandardError; end
29
29
 
30
+ class << self
31
+ extend T::Sig
32
+
33
+ sig { returns(Interface::DocumentFormattingRegistrationOptions) }
34
+ def provider
35
+ Interface::DocumentFormattingRegistrationOptions.new(
36
+ document_selector: [
37
+ Interface::DocumentFilter.new(language: "ruby"),
38
+ ],
39
+ )
40
+ end
41
+ end
42
+
30
43
  sig { params(global_state: GlobalState, document: Document).void }
31
44
  def initialize(global_state, document)
32
45
  super()
@@ -22,9 +22,9 @@ module RubyLsp
22
22
  class << self
23
23
  extend T::Sig
24
24
 
25
- sig { returns(Interface::HoverClientCapabilities) }
25
+ sig { returns(Interface::HoverOptions) }
26
26
  def provider
27
- Interface::HoverClientCapabilities.new(dynamic_registration: false)
27
+ Interface::HoverOptions.new
28
28
  end
29
29
  end
30
30
 
@@ -21,9 +21,10 @@ module RubyLsp
21
21
  class << self
22
22
  extend T::Sig
23
23
 
24
- sig { returns(Interface::DocumentOnTypeFormattingOptions) }
24
+ sig { returns(Interface::DocumentOnTypeFormattingRegistrationOptions) }
25
25
  def provider
26
- Interface::DocumentOnTypeFormattingOptions.new(
26
+ Interface::DocumentOnTypeFormattingRegistrationOptions.new(
27
+ document_selector: [Interface::DocumentFilter.new(language: "ruby")],
27
28
  first_trigger_character: "{",
28
29
  more_trigger_character: ["\n", "|", "d"],
29
30
  )
@@ -181,6 +181,7 @@ module RubyLsp
181
181
  hover_provider = Requests::Hover.provider if enabled_features["hover"]
182
182
  folding_ranges_provider = Requests::FoldingRanges.provider if enabled_features["foldingRanges"]
183
183
  semantic_tokens_provider = Requests::SemanticHighlighting.provider if enabled_features["semanticHighlighting"]
184
+ document_formatting_provider = Requests::Formatting.provider if enabled_features["formatting"]
184
185
  diagnostics_provider = Requests::Diagnostics.provider if enabled_features["diagnostics"]
185
186
  on_type_formatting_provider = Requests::OnTypeFormatting.provider if enabled_features["onTypeFormatting"]
186
187
  code_action_provider = Requests::CodeActions.provider if enabled_features["codeActions"]
@@ -202,7 +203,7 @@ module RubyLsp
202
203
  document_link_provider: document_link_provider,
203
204
  folding_range_provider: folding_ranges_provider,
204
205
  semantic_tokens_provider: semantic_tokens_provider,
205
- document_formatting_provider: enabled_features["formatting"] && @global_state.formatter != "none",
206
+ document_formatting_provider: document_formatting_provider && @global_state.formatter != "none",
206
207
  document_highlight_provider: enabled_features["documentHighlights"],
207
208
  code_action_provider: code_action_provider,
208
209
  document_on_type_formatting_provider: on_type_formatting_provider,
@@ -421,8 +422,6 @@ module RubyLsp
421
422
 
422
423
  document = @store.get(uri)
423
424
 
424
- return send_empty_response(message[:id]) if document.is_a?(ERBDocument)
425
-
426
425
  response = Requests::Formatting.new(@global_state, document).perform
427
426
  send_message(Result.new(id: message[:id], response: response))
428
427
  rescue Requests::Request::InvalidFormatter => error
@@ -448,8 +447,6 @@ module RubyLsp
448
447
  params = message[:params]
449
448
  document = @store.get(params.dig(:textDocument, :uri))
450
449
 
451
- return send_empty_response(message[:id]) if document.is_a?(ERBDocument)
452
-
453
450
  send_message(
454
451
  Result.new(
455
452
  id: message[:id],
@@ -506,8 +503,6 @@ module RubyLsp
506
503
  params = message[:params]
507
504
  document = @store.get(params.dig(:textDocument, :uri))
508
505
 
509
- return send_empty_response(message[:id]) if document.is_a?(ERBDocument)
510
-
511
506
  send_message(
512
507
  Result.new(
513
508
  id: message[:id],
@@ -563,8 +558,6 @@ module RubyLsp
563
558
 
564
559
  document = @store.get(uri)
565
560
 
566
- return send_empty_response(message[:id]) if document.is_a?(ERBDocument)
567
-
568
561
  response = document.cache_fetch("textDocument/diagnostic") do |document|
569
562
  Requests::Diagnostics.new(@global_state, document).perform
570
563
  end
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.17.10
4
+ version: 0.17.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-30 00:00:00.000000000 Z
11
+ date: 2024-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -98,6 +98,7 @@ files:
98
98
  - lib/ruby-lsp.rb
99
99
  - lib/ruby_indexer/lib/ruby_indexer/configuration.rb
100
100
  - lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb
101
+ - lib/ruby_indexer/lib/ruby_indexer/enhancement.rb
101
102
  - lib/ruby_indexer/lib/ruby_indexer/entry.rb
102
103
  - lib/ruby_indexer/lib/ruby_indexer/index.rb
103
104
  - lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb
@@ -108,6 +109,7 @@ files:
108
109
  - lib/ruby_indexer/test/classes_and_modules_test.rb
109
110
  - lib/ruby_indexer/test/configuration_test.rb
110
111
  - lib/ruby_indexer/test/constant_test.rb
112
+ - lib/ruby_indexer/test/enhancements_test.rb
111
113
  - lib/ruby_indexer/test/index_test.rb
112
114
  - lib/ruby_indexer/test/instance_variables_test.rb
113
115
  - lib/ruby_indexer/test/method_test.rb