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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +68 -0
  3. data/README.md +553 -115
  4. data/lib/ruby/lsp/refactor/version.rb +1 -1
  5. data/lib/ruby_lsp/ruby_lsp_refactor/addon.rb +54 -17
  6. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/accessor_listener.rb +156 -0
  7. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/array_listener.rb +2 -2
  8. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/conditional_listener.rb +14 -14
  9. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/constant_listener.rb +118 -0
  10. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/early_return_listener.rb +105 -0
  11. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/enumerable_listener.rb +90 -0
  12. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_include_file_listener.rb +172 -0
  13. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/extract_predicate_listener.rb +138 -0
  14. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/hash_listener.rb +7 -7
  15. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/logical_operator_listener.rb +70 -0
  16. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/method_listener.rb +37 -109
  17. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/raise_listener.rb +70 -0
  18. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/rescue_listener.rb +85 -0
  19. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/rspec_let_listener.rb +61 -0
  20. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_array_listener.rb +88 -0
  21. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_freeze_listener.rb +86 -0
  22. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/string_listener.rb +2 -2
  23. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/super_listener.rb +95 -0
  24. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/tap_listener.rb +133 -0
  25. data/lib/ruby_lsp/ruby_lsp_refactor/listeners/variable_listener.rb +7 -49
  26. data/lib/ruby_lsp/ruby_lsp_refactor/support/node_helpers.rb +62 -12
  27. data/lib/ruby_lsp/test_helper.rb +5 -5
  28. data/test/ruby_lsp_refactor/accessor_listener_test.rb +91 -0
  29. data/test/ruby_lsp_refactor/constant_listener_test.rb +68 -0
  30. data/test/ruby_lsp_refactor/early_return_listener_test.rb +156 -0
  31. data/test/ruby_lsp_refactor/enumerable_listener_test.rb +80 -0
  32. data/test/ruby_lsp_refactor/extract_include_file_listener_test.rb +189 -0
  33. data/test/ruby_lsp_refactor/extract_predicate_listener_test.rb +113 -0
  34. data/test/ruby_lsp_refactor/logical_operator_listener_test.rb +66 -0
  35. data/test/ruby_lsp_refactor/method_listener_test.rb +3 -52
  36. data/test/ruby_lsp_refactor/raise_listener_test.rb +64 -0
  37. data/test/ruby_lsp_refactor/rescue_listener_test.rb +72 -0
  38. data/test/ruby_lsp_refactor/rspec_let_listener_test.rb +54 -0
  39. data/test/ruby_lsp_refactor/string_array_listener_test.rb +64 -0
  40. data/test/ruby_lsp_refactor/string_freeze_listener_test.rb +52 -0
  41. data/test/ruby_lsp_refactor/string_listener_test.rb +2 -2
  42. data/test/ruby_lsp_refactor/super_listener_test.rb +65 -0
  43. data/test/ruby_lsp_refactor/tap_listener_test.rb +144 -0
  44. data/test/ruby_lsp_refactor/variable_listener_test.rb +0 -23
  45. 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
- new_text = "{ #{new_pairs.join(", ")} }"
72
- else
73
- new_text = new_pairs.join(", ")
74
- end
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: Constant::CodeActionKind::REFACTOR_REWRITE,
79
- edit: single_edit_workspace_edit(node, new_text),
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. Extract to method
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
- # 3. Convert to keyword arguments
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:)` and updates every
26
- # call-site within the same file that passes positional arguments.
18
+ # Rewrites `def foo(a, b)` → `def foo(a:, b:)`.
27
19
  #
28
- # 4. Extract to let (RSpec)
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. Extract to method ─────────────────────────────────────────────────
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: Interface::Position.new(line: insert_line, character: insert_col),
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: Interface::Position.new(line: name_end_line, character: name_end_col),
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: Constant::CodeActionKind::REFACTOR_REWRITE,
209
- edit: multi_edit_workspace_edit([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
- # ── 3. Convert to keyword arguments ──────────────────────────────────────
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: params_node.location.start_line - 1,
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: params_node.location.end_line - 1,
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: Constant::CodeActionKind::REFACTOR_REWRITE,
255
- edit: multi_edit_workspace_edit([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
- parts << "#{p.name}:"
266
- else
267
- parts << p.location.slice.strip
268
- end
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 if params_node.rest
273
- params_node.keywords.each { |p| parts << p.location.slice.strip }
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
- # ── 4. Extract to let (RSpec) ─────────────────────────────────────────────
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: Interface::Position.new(line: insert_line, character: 0),
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: Constant::CodeActionKind::REFACTOR_EXTRACT,
311
- edit: multi_edit_workspace_edit([insert_edit, delete_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