ruby-lsp 0.17.11 → 0.17.12
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 +4 -4
- data/VERSION +1 -1
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +19 -2
- data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +26 -0
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +23 -0
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +70 -1
- data/lib/ruby_indexer/ruby_indexer.rb +1 -0
- data/lib/ruby_indexer/test/enhancements_test.rb +197 -0
- data/lib/ruby_lsp/document.rb +34 -0
- data/lib/ruby_lsp/listeners/hover.rb +4 -1
- data/lib/ruby_lsp/requests/code_action_resolve.rb +104 -6
- data/lib/ruby_lsp/requests/code_actions.rb +6 -0
- data/lib/ruby_lsp/requests/completion_resolve.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 34cae8b53cfb0811cb26d3b3dd4198a72585d1f15a98a7d8f656fd1676886630
|
4
|
+
data.tar.gz: f4868222c46c4cbad9a099fce69562838b5dda567f1666a82752f45fe3f501c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db8e991497aa6e9388b2778fb9a417adbef07c6bb29165ff9a1718ab7cf1db6d2a6586453320d72adf0cffe78b846b553c3a0ba24b63807c52170f02f1b7d2e3
|
7
|
+
data.tar.gz: 80aac045ca6aaf7dbb9344f947a9b1419eaac4711448d997c902f0fb9da68e7c2338e0a5b1d81a30c0a2609defe1b656457d1a4a23b0ba5288162d142cf7c9e9
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.17.
|
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(
|
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(
|
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
|
data/lib/ruby_lsp/document.rb
CHANGED
@@ -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|
|
@@ -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
|
-
|
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
|
60
|
-
|
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,6 +21,7 @@ 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
|
@@ -69,6 +70,11 @@ module RubyLsp
|
|
69
70
|
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
70
71
|
data: { range: @range, uri: @uri.to_s },
|
71
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
|
+
)
|
72
78
|
end
|
73
79
|
|
74
80
|
code_actions
|
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.
|
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-08-
|
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
|