ruby-lsp-refactor 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +68 -0
- data/README.md +553 -115
- data/lib/ruby/lsp/refactor/version.rb +1 -1
- data/lib/ruby_lsp/ruby_lsp_refactor/addon.rb +54 -17
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/accessor_listener.rb +156 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/array_listener.rb +2 -2
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/conditional_listener.rb +14 -14
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/constant_listener.rb +118 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/early_return_listener.rb +105 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/enumerable_listener.rb +90 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_include_file_listener.rb +172 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_predicate_listener.rb +138 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/hash_listener.rb +7 -7
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/logical_operator_listener.rb +70 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/method_listener.rb +37 -109
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/raise_listener.rb +70 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/rescue_listener.rb +85 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/rspec_let_listener.rb +61 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_array_listener.rb +88 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_freeze_listener.rb +86 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_listener.rb +2 -2
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/super_listener.rb +95 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/tap_listener.rb +133 -0
- data/lib/ruby_lsp/ruby_lsp_refactor/listeners/variable_listener.rb +7 -49
- data/lib/ruby_lsp/ruby_lsp_refactor/support/node_helpers.rb +62 -12
- data/lib/ruby_lsp/test_helper.rb +5 -5
- data/test/ruby_lsp_refactor/accessor_listener_test.rb +91 -0
- data/test/ruby_lsp_refactor/constant_listener_test.rb +68 -0
- data/test/ruby_lsp_refactor/early_return_listener_test.rb +156 -0
- data/test/ruby_lsp_refactor/enumerable_listener_test.rb +80 -0
- data/test/ruby_lsp_refactor/extract_include_file_listener_test.rb +189 -0
- data/test/ruby_lsp_refactor/extract_predicate_listener_test.rb +113 -0
- data/test/ruby_lsp_refactor/logical_operator_listener_test.rb +66 -0
- data/test/ruby_lsp_refactor/method_listener_test.rb +3 -52
- data/test/ruby_lsp_refactor/raise_listener_test.rb +64 -0
- data/test/ruby_lsp_refactor/rescue_listener_test.rb +72 -0
- data/test/ruby_lsp_refactor/rspec_let_listener_test.rb +54 -0
- data/test/ruby_lsp_refactor/string_array_listener_test.rb +64 -0
- data/test/ruby_lsp_refactor/string_freeze_listener_test.rb +52 -0
- data/test/ruby_lsp_refactor/string_listener_test.rb +2 -2
- data/test/ruby_lsp_refactor/super_listener_test.rb +65 -0
- data/test/ruby_lsp_refactor/tap_listener_test.rb +144 -0
- data/test/ruby_lsp_refactor/variable_listener_test.rb +0 -23
- metadata +42 -13
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require_relative "../support/node_helpers"
|
|
5
|
+
|
|
6
|
+
module RubyLsp
|
|
7
|
+
module Refactor
|
|
8
|
+
# Offers "Extract to include file" when the cursor is on a ModuleNode or
|
|
9
|
+
# ClassNode that coexists with other top-level statements in the same file.
|
|
10
|
+
#
|
|
11
|
+
# The action:
|
|
12
|
+
# 1. Creates a new file named after the module/class (snake_case) in the
|
|
13
|
+
# same directory as the source file.
|
|
14
|
+
# 2. Writes the extracted node's source into the new file, prefixed with
|
|
15
|
+
# the standard `# frozen_string_literal: true` magic comment.
|
|
16
|
+
# 3. Replaces the extracted node in the source file with a
|
|
17
|
+
# `require_relative` statement.
|
|
18
|
+
#
|
|
19
|
+
# This uses `document_changes` (not `changes`) in the WorkspaceEdit so
|
|
20
|
+
# that the LSP client can handle the CreateFile resource operation.
|
|
21
|
+
#
|
|
22
|
+
# Input (cursor on the module, file also contains User class):
|
|
23
|
+
# # app/models/user.rb
|
|
24
|
+
# module Greetable
|
|
25
|
+
# def greet = "hello"
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# class User
|
|
29
|
+
# include Greetable
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# Output:
|
|
33
|
+
# # app/models/greetable.rb (new file)
|
|
34
|
+
# # frozen_string_literal: true
|
|
35
|
+
#
|
|
36
|
+
# module Greetable
|
|
37
|
+
# def greet = "hello"
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # app/models/user.rb (modified)
|
|
41
|
+
# require_relative "greetable"
|
|
42
|
+
#
|
|
43
|
+
# class User
|
|
44
|
+
# include Greetable
|
|
45
|
+
# end
|
|
46
|
+
class ExtractIncludeFileListener
|
|
47
|
+
include RubyLsp::Requests::Support::Common
|
|
48
|
+
include Support::NodeHelpers
|
|
49
|
+
|
|
50
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
51
|
+
@response_builder = response_builder
|
|
52
|
+
@node_context = node_context
|
|
53
|
+
@top_level_count = 0
|
|
54
|
+
|
|
55
|
+
dispatcher.register(
|
|
56
|
+
self,
|
|
57
|
+
:on_program_node_enter,
|
|
58
|
+
:on_module_node_enter,
|
|
59
|
+
:on_class_node_enter
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Count top-level statements so we know whether extraction is meaningful.
|
|
64
|
+
def on_program_node_enter(node)
|
|
65
|
+
@top_level_count = node.statements.body.length
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def on_module_node_enter(node)
|
|
71
|
+
return unless node_covers_cursor?(node)
|
|
72
|
+
return unless top_level_node?(node)
|
|
73
|
+
return unless @top_level_count > 1
|
|
74
|
+
|
|
75
|
+
emit_extract(node, module_name(node))
|
|
76
|
+
rescue StandardError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def on_class_node_enter(node)
|
|
81
|
+
return unless node_covers_cursor?(node)
|
|
82
|
+
return unless top_level_node?(node)
|
|
83
|
+
return unless @top_level_count > 1
|
|
84
|
+
|
|
85
|
+
emit_extract(node, class_name(node))
|
|
86
|
+
rescue StandardError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# A node is top-level when its start column is 0 (not nested inside
|
|
93
|
+
# another class/module).
|
|
94
|
+
def top_level_node?(node)
|
|
95
|
+
node.location.start_column.zero?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def module_name(node)
|
|
99
|
+
node.constant_path.location.slice
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def class_name(node)
|
|
103
|
+
node.constant_path.location.slice
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def emit_extract(node, const_name)
|
|
107
|
+
source_uri_str = @node_context.uri
|
|
108
|
+
new_uri_str = new_file_uri(source_uri_str, const_name)
|
|
109
|
+
require_name = snake_case(const_name)
|
|
110
|
+
node_src = node.location.slice
|
|
111
|
+
|
|
112
|
+
# ── 1. Create the new file ──────────────────────────────────────────
|
|
113
|
+
create_op = create_file_operation(new_uri_str)
|
|
114
|
+
|
|
115
|
+
# ── 2. Write the extracted source into the new file ─────────────────
|
|
116
|
+
new_file_content = "# frozen_string_literal: true\n\n#{node_src}\n"
|
|
117
|
+
write_new_file = text_document_edit(
|
|
118
|
+
new_uri_str,
|
|
119
|
+
[
|
|
120
|
+
Interface::TextEdit.new(
|
|
121
|
+
range: Interface::Range.new(
|
|
122
|
+
start: Interface::Position.new(line: 0, character: 0),
|
|
123
|
+
end: Interface::Position.new(line: 0, character: 0)
|
|
124
|
+
),
|
|
125
|
+
new_text: new_file_content
|
|
126
|
+
)
|
|
127
|
+
]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# ── 3. Replace the node in the source file with require_relative ────
|
|
131
|
+
# Include a trailing newline so the surrounding code stays clean.
|
|
132
|
+
require_text = "require_relative \"#{require_name}\"\n"
|
|
133
|
+
replace_in_src = text_document_edit(
|
|
134
|
+
source_uri_str,
|
|
135
|
+
[
|
|
136
|
+
Interface::TextEdit.new(
|
|
137
|
+
range: node_to_lsp_range(node),
|
|
138
|
+
new_text: require_text
|
|
139
|
+
)
|
|
140
|
+
]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
@response_builder << Interface::CodeAction.new(
|
|
144
|
+
title: "Extract to include file \"#{require_name}.rb\"",
|
|
145
|
+
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
|
146
|
+
edit: multi_file_workspace_edit([create_op, write_new_file, replace_in_src])
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Derives the URI for the new file from the source file's URI and the
|
|
151
|
+
# constant name. The new file is placed in the same directory.
|
|
152
|
+
def new_file_uri(source_uri_str, const_name)
|
|
153
|
+
filename = "#{snake_case(const_name)}.rb"
|
|
154
|
+
source_uri = URI(source_uri_str)
|
|
155
|
+
source_dir = File.dirname(source_uri.path)
|
|
156
|
+
new_path = File.join(source_dir, filename)
|
|
157
|
+
|
|
158
|
+
new_uri = source_uri.dup
|
|
159
|
+
new_uri.path = new_path
|
|
160
|
+
new_uri.to_s
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Converts CamelCase to snake_case.
|
|
164
|
+
def snake_case(name)
|
|
165
|
+
name
|
|
166
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
167
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
168
|
+
.downcase
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Offers "Extract predicate methods" when the cursor is on a compound
|
|
8
|
+
# `&&` or `||` expression that is the sole statement in a method body.
|
|
9
|
+
#
|
|
10
|
+
# From "Refactor Compound Conditionals into Methods" in Refactoring Rails:
|
|
11
|
+
# each operand becomes a private predicate method, making the condition
|
|
12
|
+
# self-documenting and each predicate independently testable.
|
|
13
|
+
#
|
|
14
|
+
# Input (cursor on the compound expression):
|
|
15
|
+
# def eligible_for_return?
|
|
16
|
+
# expired_orders.exclude?(self) && self.value > MINIMUM_RETURN_VALUE
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Output:
|
|
20
|
+
# def eligible_for_return?
|
|
21
|
+
# predicate_1? && predicate_2?
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# private
|
|
25
|
+
#
|
|
26
|
+
# def predicate_1?
|
|
27
|
+
# expired_orders.exclude?(self)
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# def predicate_2?
|
|
31
|
+
# self.value > MINIMUM_RETURN_VALUE
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# The generated names `predicate_1?` / `predicate_2?` are intentional
|
|
35
|
+
# placeholders — the developer renames them to reflect intent.
|
|
36
|
+
class ExtractPredicateListener
|
|
37
|
+
include RubyLsp::Requests::Support::Common
|
|
38
|
+
include Support::NodeHelpers
|
|
39
|
+
|
|
40
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
41
|
+
@response_builder = response_builder
|
|
42
|
+
@node_context = node_context
|
|
43
|
+
@current_def = nil
|
|
44
|
+
|
|
45
|
+
dispatcher.register(
|
|
46
|
+
self,
|
|
47
|
+
:on_def_node_enter,
|
|
48
|
+
:on_def_node_leave,
|
|
49
|
+
:on_and_node_enter,
|
|
50
|
+
:on_or_node_enter
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def on_def_node_enter(node)
|
|
55
|
+
@current_def = node
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def on_def_node_leave(_node)
|
|
61
|
+
@current_def = nil
|
|
62
|
+
rescue StandardError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def on_and_node_enter(node)
|
|
67
|
+
return unless node_covers_cursor?(node)
|
|
68
|
+
return unless sole_statement_in_def?(node)
|
|
69
|
+
|
|
70
|
+
emit_extract_predicates(node, "&&")
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def on_or_node_enter(node)
|
|
76
|
+
return unless node_covers_cursor?(node)
|
|
77
|
+
return unless sole_statement_in_def?(node)
|
|
78
|
+
|
|
79
|
+
emit_extract_predicates(node, "||")
|
|
80
|
+
rescue StandardError
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# The compound expression must be the only statement in the method body
|
|
87
|
+
# so the replacement is unambiguous.
|
|
88
|
+
def sole_statement_in_def?(node)
|
|
89
|
+
return false unless @current_def
|
|
90
|
+
|
|
91
|
+
body = @current_def.body
|
|
92
|
+
return false unless body.is_a?(Prism::StatementsNode)
|
|
93
|
+
return false unless body.body.length == 1
|
|
94
|
+
|
|
95
|
+
body.body.first.equal?(node)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def emit_extract_predicates(node, operator)
|
|
99
|
+
def_node = @current_def
|
|
100
|
+
indent = indent_for(def_node)
|
|
101
|
+
body_indent = "#{indent} "
|
|
102
|
+
|
|
103
|
+
left_src = node.left.location.slice.strip
|
|
104
|
+
right_src = node.right.location.slice.strip
|
|
105
|
+
|
|
106
|
+
# Replace the compound expression with calls to the two new predicates.
|
|
107
|
+
replace_edit = Interface::TextEdit.new(
|
|
108
|
+
range: node_to_lsp_range(node),
|
|
109
|
+
new_text: "#{body_indent}predicate_1? #{operator} predicate_2?"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Insert the two private predicate methods after the enclosing def.
|
|
113
|
+
insert_line = def_node.location.end_line
|
|
114
|
+
new_methods = "\n#{indent}private\n\n" \
|
|
115
|
+
"#{indent}def predicate_1?\n" \
|
|
116
|
+
"#{body_indent}#{left_src}\n" \
|
|
117
|
+
"#{indent}end\n\n" \
|
|
118
|
+
"#{indent}def predicate_2?\n" \
|
|
119
|
+
"#{body_indent}#{right_src}\n" \
|
|
120
|
+
"#{indent}end\n"
|
|
121
|
+
|
|
122
|
+
insert_edit = Interface::TextEdit.new(
|
|
123
|
+
range: Interface::Range.new(
|
|
124
|
+
start: Interface::Position.new(line: insert_line, character: 0),
|
|
125
|
+
end: Interface::Position.new(line: insert_line, character: 0)
|
|
126
|
+
),
|
|
127
|
+
new_text: new_methods
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@response_builder << Interface::CodeAction.new(
|
|
131
|
+
title: "Extract predicate methods",
|
|
132
|
+
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
|
133
|
+
edit: multi_edit_workspace_edit([replace_edit, insert_edit])
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -67,16 +67,16 @@ module RubyLsp
|
|
|
67
67
|
|
|
68
68
|
# Reconstruct the hash preserving the outer braces when present.
|
|
69
69
|
# HashNode has opening/closing braces; KeywordHashNode does not.
|
|
70
|
-
if node.is_a?(Prism::HashNode)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
new_text = if node.is_a?(Prism::HashNode)
|
|
71
|
+
"{ #{new_pairs.join(", ")} }"
|
|
72
|
+
else
|
|
73
|
+
new_pairs.join(", ")
|
|
74
|
+
end
|
|
75
75
|
|
|
76
76
|
@response_builder << Interface::CodeAction.new(
|
|
77
77
|
title: "Convert to keyword syntax",
|
|
78
|
-
kind:
|
|
79
|
-
edit:
|
|
78
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
79
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
80
80
|
)
|
|
81
81
|
end
|
|
82
82
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Toggles between symbolic and word forms of logical operators.
|
|
8
|
+
#
|
|
9
|
+
# Emitted actions
|
|
10
|
+
# ───────────────
|
|
11
|
+
# AndNode:
|
|
12
|
+
# user.valid? && user.save → user.valid? and user.save
|
|
13
|
+
# user.valid? and user.save → user.valid? && user.save
|
|
14
|
+
#
|
|
15
|
+
# OrNode:
|
|
16
|
+
# a || b → a or b
|
|
17
|
+
# a or b → a || b
|
|
18
|
+
class LogicalOperatorListener
|
|
19
|
+
include RubyLsp::Requests::Support::Common
|
|
20
|
+
include Support::NodeHelpers
|
|
21
|
+
|
|
22
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
23
|
+
@response_builder = response_builder
|
|
24
|
+
@node_context = node_context
|
|
25
|
+
|
|
26
|
+
dispatcher.register(self, :on_and_node_enter, :on_or_node_enter)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def on_and_node_enter(node)
|
|
30
|
+
return unless node_covers_cursor?(node)
|
|
31
|
+
|
|
32
|
+
if node.operator_loc.slice == "&&"
|
|
33
|
+
emit_toggle(node, "&&", "and")
|
|
34
|
+
else
|
|
35
|
+
emit_toggle(node, "and", "&&")
|
|
36
|
+
end
|
|
37
|
+
rescue StandardError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def on_or_node_enter(node)
|
|
42
|
+
return unless node_covers_cursor?(node)
|
|
43
|
+
|
|
44
|
+
if node.operator_loc.slice == "||"
|
|
45
|
+
emit_toggle(node, "||", "or")
|
|
46
|
+
else
|
|
47
|
+
emit_toggle(node, "or", "||")
|
|
48
|
+
end
|
|
49
|
+
rescue StandardError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def emit_toggle(node, from_op, to_op)
|
|
56
|
+
left_src = node.left.location.slice.strip
|
|
57
|
+
right_src = node.right.location.slice.strip
|
|
58
|
+
new_text = "#{indent_for(node)}#{left_src} #{to_op} #{right_src}"
|
|
59
|
+
|
|
60
|
+
title = "Convert '#{from_op}' to '#{to_op}'"
|
|
61
|
+
|
|
62
|
+
@response_builder << Interface::CodeAction.new(
|
|
63
|
+
title: title,
|
|
64
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
65
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -8,27 +8,22 @@ module RubyLsp
|
|
|
8
8
|
#
|
|
9
9
|
# Emitted actions
|
|
10
10
|
# ───────────────
|
|
11
|
-
# 1.
|
|
12
|
-
# Cursor on a LocalVariableWriteNode inside a def body.
|
|
13
|
-
# Extracts the assignment RHS (and the assignment itself) into a new
|
|
14
|
-
# private method, passing any variables that are defined before the
|
|
15
|
-
# extraction point and referenced inside the extracted expression as
|
|
16
|
-
# parameters.
|
|
17
|
-
#
|
|
18
|
-
# 2. Add parameter
|
|
11
|
+
# 1. Add parameter
|
|
19
12
|
# Cursor anywhere inside a DefNode.
|
|
20
13
|
# Appends a `new_param` placeholder at the end of the parameter list
|
|
21
14
|
# (or creates parentheses if the method has none).
|
|
22
15
|
#
|
|
23
|
-
#
|
|
16
|
+
# 2. Convert to keyword arguments
|
|
24
17
|
# Cursor anywhere inside a DefNode that has required positional params.
|
|
25
|
-
# Rewrites `def foo(a, b)` → `def foo(a:, b:)
|
|
26
|
-
# call-site within the same file that passes positional arguments.
|
|
18
|
+
# Rewrites `def foo(a, b)` → `def foo(a:, b:)`.
|
|
27
19
|
#
|
|
28
|
-
#
|
|
20
|
+
# 3. Extract to let (RSpec)
|
|
29
21
|
# Cursor on a LocalVariableWriteNode inside an RSpec `it`/`specify`
|
|
30
22
|
# block. Moves the assignment into a `let(:name) { value }` block
|
|
31
23
|
# inserted above the enclosing example group call.
|
|
24
|
+
#
|
|
25
|
+
# Note: "Extract to method" is provided by ruby-lsp upstream as
|
|
26
|
+
# "Refactor: Extract Method" and is intentionally not duplicated here.
|
|
32
27
|
class MethodListener
|
|
33
28
|
include RubyLsp::Requests::Support::Common
|
|
34
29
|
include Support::NodeHelpers
|
|
@@ -44,16 +39,13 @@ module RubyLsp
|
|
|
44
39
|
# Each entry is a Hash with :type and the node itself.
|
|
45
40
|
@ancestor_stack = []
|
|
46
41
|
|
|
47
|
-
# All write nodes seen so far (for "defined before extraction point").
|
|
48
|
-
@seen_writes = []
|
|
49
|
-
|
|
50
42
|
dispatcher.register(
|
|
51
43
|
self,
|
|
52
44
|
:on_def_node_enter,
|
|
53
45
|
:on_def_node_leave,
|
|
54
46
|
:on_call_node_enter,
|
|
55
47
|
:on_call_node_leave,
|
|
56
|
-
:on_local_variable_write_node_enter
|
|
48
|
+
:on_local_variable_write_node_enter
|
|
57
49
|
)
|
|
58
50
|
end
|
|
59
51
|
|
|
@@ -74,7 +66,6 @@ module RubyLsp
|
|
|
74
66
|
|
|
75
67
|
def on_def_node_leave(_node)
|
|
76
68
|
@ancestor_stack.pop
|
|
77
|
-
@seen_writes.clear
|
|
78
69
|
rescue StandardError
|
|
79
70
|
nil
|
|
80
71
|
end
|
|
@@ -92,19 +83,11 @@ module RubyLsp
|
|
|
92
83
|
end
|
|
93
84
|
|
|
94
85
|
def on_local_variable_write_node_enter(node)
|
|
95
|
-
enclosing_def = nearest_ancestor(:def)
|
|
96
86
|
enclosing_call = nearest_ancestor(:call)
|
|
97
87
|
|
|
98
|
-
# Always track writes for param-detection, regardless of cursor position.
|
|
99
|
-
@seen_writes << node
|
|
100
|
-
|
|
101
88
|
return unless node_covers_cursor?(node)
|
|
102
89
|
|
|
103
|
-
if enclosing_call && rspec_example?(enclosing_call[:node])
|
|
104
|
-
emit_extract_to_let(node, enclosing_call[:node])
|
|
105
|
-
elsif enclosing_def
|
|
106
|
-
emit_extract_to_method(node, enclosing_def[:node])
|
|
107
|
-
end
|
|
90
|
+
emit_extract_to_let(node, enclosing_call[:node]) if enclosing_call && rspec_example?(enclosing_call[:node])
|
|
108
91
|
rescue StandardError
|
|
109
92
|
nil
|
|
110
93
|
end
|
|
@@ -117,62 +100,7 @@ module RubyLsp
|
|
|
117
100
|
@ancestor_stack.reverse.find { |a| a[:type] == type }
|
|
118
101
|
end
|
|
119
102
|
|
|
120
|
-
# ── 1.
|
|
121
|
-
|
|
122
|
-
def emit_extract_to_method(write_node, def_node)
|
|
123
|
-
method_name = write_node.name.to_s
|
|
124
|
-
rhs_src = write_node.value.location.slice.strip
|
|
125
|
-
indent = indent_for(def_node)
|
|
126
|
-
body_indent = "#{indent} "
|
|
127
|
-
|
|
128
|
-
# Determine which variables defined before this write are referenced
|
|
129
|
-
# inside the RHS expression.
|
|
130
|
-
params = params_needed_for(write_node.value, def_node)
|
|
131
|
-
param_list = params.empty? ? "" : "(#{params.join(", ")})"
|
|
132
|
-
call_args = params.empty? ? "" : "(#{params.join(", ")})"
|
|
133
|
-
|
|
134
|
-
# Replace the assignment RHS with a call to the new method.
|
|
135
|
-
replace_edit = Interface::TextEdit.new(
|
|
136
|
-
range: node_to_lsp_range(write_node.value),
|
|
137
|
-
new_text: "#{method_name}#{call_args}",
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
# Insert the new private method after the enclosing def's closing `end`.
|
|
141
|
-
insert_line = def_node.location.end_line # 1-based; insert after this line
|
|
142
|
-
new_method = "\n#{body_indent}private\n\n" \
|
|
143
|
-
"#{body_indent}def #{method_name}#{param_list}\n" \
|
|
144
|
-
"#{body_indent} #{rhs_src}\n" \
|
|
145
|
-
"#{body_indent}end\n"
|
|
146
|
-
|
|
147
|
-
insert_edit = Interface::TextEdit.new(
|
|
148
|
-
range: Interface::Range.new(
|
|
149
|
-
start: Interface::Position.new(line: insert_line, character: 0),
|
|
150
|
-
end: Interface::Position.new(line: insert_line, character: 0),
|
|
151
|
-
),
|
|
152
|
-
new_text: new_method,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
@response_builder << Interface::CodeAction.new(
|
|
156
|
-
title: "Extract to method '#{method_name}'",
|
|
157
|
-
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
|
158
|
-
edit: multi_edit_workspace_edit([replace_edit, insert_edit]),
|
|
159
|
-
)
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Collect names of variables that are:
|
|
163
|
-
# a) written before the extraction point in the same def body, AND
|
|
164
|
-
# b) referenced (read) inside +expr_node+.
|
|
165
|
-
def params_needed_for(expr_node, def_node)
|
|
166
|
-
expr_src = expr_node.location.slice
|
|
167
|
-
|
|
168
|
-
@seen_writes
|
|
169
|
-
.select { |w| w.location.start_offset < expr_node.location.start_offset }
|
|
170
|
-
.map { |w| w.name.to_s }
|
|
171
|
-
.select { |name| expr_src.match?(/\b#{Regexp.escape(name)}\b/) }
|
|
172
|
-
.uniq
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# ── 2. Add parameter ─────────────────────────────────────────────────────
|
|
103
|
+
# ── 1. Add parameter ─────────────────────────────────────────────────────
|
|
176
104
|
|
|
177
105
|
def emit_add_parameter(def_node)
|
|
178
106
|
if def_node.parameters
|
|
@@ -185,9 +113,9 @@ module RubyLsp
|
|
|
185
113
|
edit = Interface::TextEdit.new(
|
|
186
114
|
range: Interface::Range.new(
|
|
187
115
|
start: Interface::Position.new(line: insert_line, character: insert_col),
|
|
188
|
-
end:
|
|
116
|
+
end: Interface::Position.new(line: insert_line, character: insert_col)
|
|
189
117
|
),
|
|
190
|
-
new_text: new_text_fragment
|
|
118
|
+
new_text: new_text_fragment
|
|
191
119
|
)
|
|
192
120
|
else
|
|
193
121
|
# No parameters yet — insert `(new_param)` right after the method name.
|
|
@@ -197,16 +125,16 @@ module RubyLsp
|
|
|
197
125
|
edit = Interface::TextEdit.new(
|
|
198
126
|
range: Interface::Range.new(
|
|
199
127
|
start: Interface::Position.new(line: name_end_line, character: name_end_col),
|
|
200
|
-
end:
|
|
128
|
+
end: Interface::Position.new(line: name_end_line, character: name_end_col)
|
|
201
129
|
),
|
|
202
|
-
new_text: "(new_param)"
|
|
130
|
+
new_text: "(new_param)"
|
|
203
131
|
)
|
|
204
132
|
end
|
|
205
133
|
|
|
206
134
|
@response_builder << Interface::CodeAction.new(
|
|
207
135
|
title: "Add parameter",
|
|
208
|
-
kind:
|
|
209
|
-
edit:
|
|
136
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
137
|
+
edit: multi_edit_workspace_edit([edit])
|
|
210
138
|
)
|
|
211
139
|
end
|
|
212
140
|
|
|
@@ -216,11 +144,11 @@ module RubyLsp
|
|
|
216
144
|
params_node.requireds,
|
|
217
145
|
params_node.optionals,
|
|
218
146
|
params_node.keywords,
|
|
219
|
-
[params_node.rest, params_node.keyword_rest, params_node.block].compact
|
|
147
|
+
[params_node.rest, params_node.keyword_rest, params_node.block].compact
|
|
220
148
|
].flatten.compact.last
|
|
221
149
|
end
|
|
222
150
|
|
|
223
|
-
# ──
|
|
151
|
+
# ── 2. Convert to keyword arguments ──────────────────────────────────────
|
|
224
152
|
|
|
225
153
|
def has_positional_params?(def_node)
|
|
226
154
|
def_node.parameters&.requireds&.any? { |p| p.is_a?(Prism::RequiredParameterNode) }
|
|
@@ -238,21 +166,21 @@ module RubyLsp
|
|
|
238
166
|
# Replace the entire parameters span (between the parens).
|
|
239
167
|
params_range = Interface::Range.new(
|
|
240
168
|
start: Interface::Position.new(
|
|
241
|
-
line:
|
|
242
|
-
character: params_node.location.start_column
|
|
169
|
+
line: params_node.location.start_line - 1,
|
|
170
|
+
character: params_node.location.start_column
|
|
243
171
|
),
|
|
244
172
|
end: Interface::Position.new(
|
|
245
|
-
line:
|
|
246
|
-
character: params_node.location.end_column
|
|
247
|
-
)
|
|
173
|
+
line: params_node.location.end_line - 1,
|
|
174
|
+
character: params_node.location.end_column
|
|
175
|
+
)
|
|
248
176
|
)
|
|
249
177
|
|
|
250
178
|
edit = Interface::TextEdit.new(range: params_range, new_text: new_params)
|
|
251
179
|
|
|
252
180
|
@response_builder << Interface::CodeAction.new(
|
|
253
181
|
title: "Convert to keyword arguments",
|
|
254
|
-
kind:
|
|
255
|
-
edit:
|
|
182
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
183
|
+
edit: multi_edit_workspace_edit([edit])
|
|
256
184
|
)
|
|
257
185
|
end
|
|
258
186
|
|
|
@@ -261,23 +189,23 @@ module RubyLsp
|
|
|
261
189
|
parts = []
|
|
262
190
|
|
|
263
191
|
params_node.requireds.each do |p|
|
|
264
|
-
if required_names.include?(p.name)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
192
|
+
parts << if required_names.include?(p.name)
|
|
193
|
+
"#{p.name}:"
|
|
194
|
+
else
|
|
195
|
+
p.location.slice.strip
|
|
196
|
+
end
|
|
269
197
|
end
|
|
270
198
|
|
|
271
199
|
params_node.optionals.each { |p| parts << p.location.slice.strip }
|
|
272
|
-
parts << params_node.rest.location.slice.strip
|
|
273
|
-
params_node.keywords.each
|
|
200
|
+
parts << params_node.rest.location.slice.strip if params_node.rest
|
|
201
|
+
params_node.keywords.each { |p| parts << p.location.slice.strip }
|
|
274
202
|
parts << params_node.keyword_rest.location.slice.strip if params_node.keyword_rest
|
|
275
203
|
parts << params_node.block.location.slice.strip if params_node.block
|
|
276
204
|
|
|
277
205
|
parts
|
|
278
206
|
end
|
|
279
207
|
|
|
280
|
-
# ──
|
|
208
|
+
# ── 3. Extract to let (RSpec) ─────────────────────────────────────────────
|
|
281
209
|
|
|
282
210
|
RSPEC_EXAMPLE_METHODS = %i[it specify example scenario].freeze
|
|
283
211
|
|
|
@@ -297,9 +225,9 @@ module RubyLsp
|
|
|
297
225
|
insert_edit = Interface::TextEdit.new(
|
|
298
226
|
range: Interface::Range.new(
|
|
299
227
|
start: Interface::Position.new(line: insert_line, character: 0),
|
|
300
|
-
end:
|
|
228
|
+
end: Interface::Position.new(line: insert_line, character: 0)
|
|
301
229
|
),
|
|
302
|
-
new_text: let_text
|
|
230
|
+
new_text: let_text
|
|
303
231
|
)
|
|
304
232
|
|
|
305
233
|
# Delete the original assignment line inside the example.
|
|
@@ -307,8 +235,8 @@ module RubyLsp
|
|
|
307
235
|
|
|
308
236
|
@response_builder << Interface::CodeAction.new(
|
|
309
237
|
title: "Extract to let(:#{var_name})",
|
|
310
|
-
kind:
|
|
311
|
-
edit:
|
|
238
|
+
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
|
239
|
+
edit: multi_edit_workspace_edit([insert_edit, delete_edit])
|
|
312
240
|
)
|
|
313
241
|
end
|
|
314
242
|
end
|