ruby-lsp-refactor 0.1.0 → 0.1.1
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/lib/ruby/lsp/refactor/version.rb +1 -1
- data/lib/ruby_lsp/ruby_lsp_refactor/addon.rb +35 -9
- 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/block_style_listener.rb +117 -0
- 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/enumerable_listener.rb +90 -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 +34 -34
- 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/variable_listener.rb +11 -11
- data/lib/ruby_lsp/ruby_lsp_refactor/support/node_helpers.rb +12 -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/block_style_listener_test.rb +98 -0
- data/test/ruby_lsp_refactor/constant_listener_test.rb +68 -0
- data/test/ruby_lsp_refactor/enumerable_listener_test.rb +80 -0
- data/test/ruby_lsp_refactor/logical_operator_listener_test.rb +66 -0
- 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
- metadata +36 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b5c862a6cc032cef40f72215c0dd23fdf603dfac480f06d40dbcb44aaf90b70
|
|
4
|
+
data.tar.gz: e54a99c4047ca609133ffa416bb48b03764404a88582268c7a02c31c0d22fb5d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 42ae1d9fdb145a816069ab5aed98e2b3dd5167dabcc8e980a74d3658d8eb4ae8a6fc78a5d6e2a5db1312b08ad19f245b7d049057eb7cab8254276dab60b6e43a
|
|
7
|
+
data.tar.gz: 552ac629c91808223461f1ff6ac3ed806815dc1a11895ad041f728b144828f35fd9470e67c8a7c6e1f5349bf0ea86848d2b28be33ac2d17d66d0dc1a817cbb15
|
data/.rubocop_todo.yml
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# This configuration was generated by
|
|
2
|
+
# `rubocop --auto-gen-config`
|
|
3
|
+
# on 2026-06-15 16:00:25 UTC using RuboCop version 1.87.0.
|
|
4
|
+
# The point is for the user to remove these configuration records
|
|
5
|
+
# one by one as the offenses are removed from the code base.
|
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
|
8
|
+
|
|
9
|
+
# Offense count: 19
|
|
10
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
|
11
|
+
Metrics/AbcSize:
|
|
12
|
+
Max: 42
|
|
13
|
+
|
|
14
|
+
# Offense count: 4
|
|
15
|
+
# Configuration parameters: CountComments, CountAsOne.
|
|
16
|
+
Metrics/ClassLength:
|
|
17
|
+
Max: 218
|
|
18
|
+
|
|
19
|
+
# Offense count: 5
|
|
20
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
21
|
+
Metrics/CyclomaticComplexity:
|
|
22
|
+
Max: 11
|
|
23
|
+
|
|
24
|
+
# Offense count: 42
|
|
25
|
+
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
26
|
+
Metrics/MethodLength:
|
|
27
|
+
Max: 36
|
|
28
|
+
|
|
29
|
+
# Offense count: 4
|
|
30
|
+
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
31
|
+
Metrics/PerceivedComplexity:
|
|
32
|
+
Max: 11
|
|
33
|
+
|
|
34
|
+
# Offense count: 2
|
|
35
|
+
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
|
36
|
+
# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
|
|
37
|
+
Naming/MethodParameterName:
|
|
38
|
+
Exclude:
|
|
39
|
+
- 'lib/ruby_lsp/ruby_lsp_refactor/listeners/hash_listener.rb'
|
|
40
|
+
|
|
41
|
+
# Offense count: 3
|
|
42
|
+
# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs.
|
|
43
|
+
# NamePrefix: is_, has_, have_, does_
|
|
44
|
+
# ForbiddenPrefixes: is_, has_, have_, does_
|
|
45
|
+
# AllowedMethods: is_a?
|
|
46
|
+
# MethodDefinitionMacros: define_method, define_singleton_method
|
|
47
|
+
Naming/PredicatePrefix:
|
|
48
|
+
Exclude:
|
|
49
|
+
- 'spec/**/*'
|
|
50
|
+
- 'lib/ruby_lsp/ruby_lsp_refactor/listeners/hash_listener.rb'
|
|
51
|
+
- 'lib/ruby_lsp/ruby_lsp_refactor/listeners/method_listener.rb'
|
|
52
|
+
- 'lib/ruby_lsp/ruby_lsp_refactor/listeners/super_listener.rb'
|
|
53
|
+
|
|
54
|
+
# Offense count: 1
|
|
55
|
+
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
|
56
|
+
# SupportedStyles: snake_case, normalcase, non_integer
|
|
57
|
+
# AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
|
58
|
+
Naming/VariableNumber:
|
|
59
|
+
Exclude:
|
|
60
|
+
- 'test/ruby_lsp_refactor/enumerable_listener_test.rb'
|
|
61
|
+
|
|
62
|
+
# Offense count: 1
|
|
63
|
+
# Configuration parameters: AllowedConstants.
|
|
64
|
+
Style/Documentation:
|
|
65
|
+
Exclude:
|
|
66
|
+
- 'spec/**/*'
|
|
67
|
+
- 'test/**/*'
|
|
68
|
+
- 'lib/ruby_lsp/ruby_lsp_refactor/addon.rb'
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "ruby_lsp/addon"
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
# Phase 1 – Local rewrites
|
|
6
6
|
require_relative "listeners/conditional_listener"
|
|
7
|
-
require_relative "listeners/variable_listener"
|
|
8
7
|
require_relative "listeners/string_listener"
|
|
8
|
+
require_relative "listeners/block_style_listener"
|
|
9
|
+
require_relative "listeners/logical_operator_listener"
|
|
10
|
+
|
|
11
|
+
# Phase 2 – Variable & literal optimisation
|
|
12
|
+
require_relative "listeners/variable_listener"
|
|
9
13
|
require_relative "listeners/hash_listener"
|
|
10
14
|
require_relative "listeners/array_listener"
|
|
15
|
+
require_relative "listeners/string_array_listener"
|
|
16
|
+
require_relative "listeners/string_freeze_listener"
|
|
17
|
+
require_relative "listeners/enumerable_listener"
|
|
18
|
+
require_relative "listeners/raise_listener"
|
|
19
|
+
|
|
20
|
+
# Phase 3 – Advanced structure
|
|
11
21
|
require_relative "listeners/method_listener"
|
|
22
|
+
require_relative "listeners/constant_listener"
|
|
23
|
+
require_relative "listeners/accessor_listener"
|
|
24
|
+
require_relative "listeners/rescue_listener"
|
|
25
|
+
require_relative "listeners/super_listener"
|
|
26
|
+
require_relative "listeners/rspec_let_listener"
|
|
12
27
|
|
|
13
28
|
module RubyLsp
|
|
14
29
|
module Refactor
|
|
@@ -31,7 +46,7 @@ module RubyLsp
|
|
|
31
46
|
|
|
32
47
|
class Addon < ::RubyLsp::Addon
|
|
33
48
|
# Called once when the language server activates this add-on.
|
|
34
|
-
def activate(global_state,
|
|
49
|
+
def activate(global_state, _message_queue)
|
|
35
50
|
@global_state = global_state
|
|
36
51
|
|
|
37
52
|
# Inject our actions into the standard code-actions response.
|
|
@@ -62,13 +77,13 @@ module RubyLsp
|
|
|
62
77
|
|
|
63
78
|
cursor_range = Interface::Range.new(
|
|
64
79
|
start: Interface::Position.new(
|
|
65
|
-
line:
|
|
66
|
-
character: range.dig(:start, :character)
|
|
80
|
+
line: range.dig(:start, :line),
|
|
81
|
+
character: range.dig(:start, :character)
|
|
67
82
|
),
|
|
68
83
|
end: Interface::Position.new(
|
|
69
|
-
line:
|
|
70
|
-
character: range.dig(:end, :character)
|
|
71
|
-
)
|
|
84
|
+
line: range.dig(:end, :line),
|
|
85
|
+
character: range.dig(:end, :character)
|
|
86
|
+
)
|
|
72
87
|
)
|
|
73
88
|
|
|
74
89
|
node_context = NodeContext.new(document.uri.to_s, cursor_range)
|
|
@@ -78,14 +93,25 @@ module RubyLsp
|
|
|
78
93
|
# Phase 1 – Local rewrites
|
|
79
94
|
ConditionalListener.new(response_builder, node_context, dispatcher)
|
|
80
95
|
StringListener.new(response_builder, node_context, dispatcher)
|
|
96
|
+
BlockStyleListener.new(response_builder, node_context, dispatcher)
|
|
97
|
+
LogicalOperatorListener.new(response_builder, node_context, dispatcher)
|
|
81
98
|
|
|
82
99
|
# Phase 2 – Variable & literal optimisation
|
|
83
100
|
VariableListener.new(response_builder, node_context, dispatcher)
|
|
84
101
|
HashListener.new(response_builder, node_context, dispatcher)
|
|
85
102
|
ArrayListener.new(response_builder, node_context, dispatcher)
|
|
103
|
+
StringArrayListener.new(response_builder, node_context, dispatcher)
|
|
104
|
+
StringFreezeListener.new(response_builder, node_context, dispatcher)
|
|
105
|
+
EnumerableListener.new(response_builder, node_context, dispatcher)
|
|
106
|
+
RaiseListener.new(response_builder, node_context, dispatcher)
|
|
86
107
|
|
|
87
108
|
# Phase 3 – Advanced structure
|
|
88
109
|
MethodListener.new(response_builder, node_context, dispatcher)
|
|
110
|
+
ConstantListener.new(response_builder, node_context, dispatcher)
|
|
111
|
+
AccessorListener.new(response_builder, node_context, dispatcher)
|
|
112
|
+
RescueListener.new(response_builder, node_context, dispatcher)
|
|
113
|
+
SuperListener.new(response_builder, node_context, dispatcher)
|
|
114
|
+
RspecLetListener.new(response_builder, node_context, dispatcher)
|
|
89
115
|
|
|
90
116
|
dispatcher.dispatch(document.ast)
|
|
91
117
|
response_builder.response
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Detects an `attr_reader :name` paired with a canonical manual writer
|
|
8
|
+
# `def name=(val); @name = val; end` in the same class body and offers
|
|
9
|
+
# to collapse them into a single `attr_accessor :name`.
|
|
10
|
+
#
|
|
11
|
+
# Input (cursor on either the attr_reader or the writer def):
|
|
12
|
+
# attr_reader :name
|
|
13
|
+
# def name=(val)
|
|
14
|
+
# @name = val
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Output:
|
|
18
|
+
# attr_accessor :name
|
|
19
|
+
#
|
|
20
|
+
# The manual writer is considered canonical when its body contains exactly
|
|
21
|
+
# one statement of the form `@name = val` where `val` is the sole parameter.
|
|
22
|
+
class AccessorListener
|
|
23
|
+
include RubyLsp::Requests::Support::Common
|
|
24
|
+
include Support::NodeHelpers
|
|
25
|
+
|
|
26
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
27
|
+
@response_builder = response_builder
|
|
28
|
+
@node_context = node_context
|
|
29
|
+
|
|
30
|
+
# Collect attr_reader calls and writer defs within the same class body.
|
|
31
|
+
@attr_readers = [] # [{ name:, node: }]
|
|
32
|
+
@writer_defs = [] # [{ name:, node: }]
|
|
33
|
+
@class_depth = 0
|
|
34
|
+
|
|
35
|
+
dispatcher.register(
|
|
36
|
+
self,
|
|
37
|
+
:on_class_node_enter,
|
|
38
|
+
:on_class_node_leave,
|
|
39
|
+
:on_call_node_enter,
|
|
40
|
+
:on_def_node_enter
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def on_class_node_enter(_node)
|
|
45
|
+
@class_depth += 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def on_class_node_leave(_node)
|
|
49
|
+
@class_depth -= 1
|
|
50
|
+
# Emit any matched pairs before clearing — on_program_node_leave fires
|
|
51
|
+
# after on_class_node_leave, so we must act here while data is present.
|
|
52
|
+
emit_matching_pairs
|
|
53
|
+
@attr_readers.clear
|
|
54
|
+
@writer_defs.clear
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def on_call_node_enter(node)
|
|
58
|
+
return unless @class_depth.positive?
|
|
59
|
+
return unless node.name == :attr_reader
|
|
60
|
+
return unless node.arguments
|
|
61
|
+
|
|
62
|
+
node.arguments.arguments.each do |arg|
|
|
63
|
+
next unless arg.is_a?(Prism::SymbolNode)
|
|
64
|
+
|
|
65
|
+
@attr_readers << { name: arg.unescaped.to_sym, node: node, sym_node: arg }
|
|
66
|
+
end
|
|
67
|
+
rescue StandardError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def on_def_node_enter(node)
|
|
72
|
+
return unless @class_depth.positive?
|
|
73
|
+
return unless node.name.to_s.end_with?("=")
|
|
74
|
+
|
|
75
|
+
attr_name = node.name.to_s.delete_suffix("=").to_sym
|
|
76
|
+
return unless canonical_writer?(node, attr_name)
|
|
77
|
+
|
|
78
|
+
@writer_defs << { name: attr_name, node: node }
|
|
79
|
+
rescue StandardError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Match readers with writers and emit actions for any pair where the
|
|
86
|
+
# cursor falls on either node.
|
|
87
|
+
def emit_matching_pairs
|
|
88
|
+
@attr_readers.each do |reader|
|
|
89
|
+
writer = @writer_defs.find { |w| w[:name] == reader[:name] }
|
|
90
|
+
next unless writer
|
|
91
|
+
|
|
92
|
+
next unless node_covers_cursor?(reader[:node]) ||
|
|
93
|
+
node_covers_cursor?(writer[:node])
|
|
94
|
+
|
|
95
|
+
emit_collapse(reader, writer)
|
|
96
|
+
end
|
|
97
|
+
rescue StandardError
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A writer def is canonical when:
|
|
102
|
+
# 1. It has exactly one required parameter.
|
|
103
|
+
# 2. Its body is exactly one statement: `@name = <param>`.
|
|
104
|
+
def canonical_writer?(def_node, attr_name)
|
|
105
|
+
params = def_node.parameters&.requireds
|
|
106
|
+
return false unless params&.length == 1
|
|
107
|
+
|
|
108
|
+
param_name = params.first.name
|
|
109
|
+
body_stmts = def_node.body&.body
|
|
110
|
+
return false unless body_stmts&.length == 1
|
|
111
|
+
|
|
112
|
+
stmt = body_stmts.first
|
|
113
|
+
return false unless stmt.is_a?(Prism::InstanceVariableWriteNode)
|
|
114
|
+
return false unless stmt.name.to_s == "@#{attr_name}"
|
|
115
|
+
return false unless stmt.value.is_a?(Prism::LocalVariableReadNode)
|
|
116
|
+
return false unless stmt.value.name == param_name
|
|
117
|
+
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def emit_collapse(reader, writer)
|
|
122
|
+
reader_node = reader[:node]
|
|
123
|
+
writer_node = writer[:node]
|
|
124
|
+
attr_name = reader[:name]
|
|
125
|
+
|
|
126
|
+
# Replace the attr_reader line with attr_accessor.
|
|
127
|
+
# The sym_node is the :name argument; we keep the same symbol.
|
|
128
|
+
reader_src = reader_node.location.slice
|
|
129
|
+
new_reader = reader_src.sub("attr_reader", "attr_accessor")
|
|
130
|
+
|
|
131
|
+
replace_reader = Interface::TextEdit.new(
|
|
132
|
+
range: node_to_lsp_range(reader_node),
|
|
133
|
+
new_text: new_reader
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Delete the entire writer def (all its lines).
|
|
137
|
+
writer_start_line = writer_node.location.start_line - 1
|
|
138
|
+
writer_end_line = writer_node.location.end_line
|
|
139
|
+
|
|
140
|
+
delete_writer = Interface::TextEdit.new(
|
|
141
|
+
range: Interface::Range.new(
|
|
142
|
+
start: Interface::Position.new(line: writer_start_line, character: 0),
|
|
143
|
+
end: Interface::Position.new(line: writer_end_line, character: 0)
|
|
144
|
+
),
|
|
145
|
+
new_text: ""
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@response_builder << Interface::CodeAction.new(
|
|
149
|
+
title: "Convert to attr_accessor :#{attr_name}",
|
|
150
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
151
|
+
edit: multi_edit_workspace_edit([replace_reader, delete_writer])
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -56,8 +56,8 @@ module RubyLsp
|
|
|
56
56
|
|
|
57
57
|
@response_builder << Interface::CodeAction.new(
|
|
58
58
|
title: "Convert to symbol array",
|
|
59
|
-
kind:
|
|
60
|
-
edit:
|
|
59
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
60
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
61
61
|
)
|
|
62
62
|
end
|
|
63
63
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Emits block-style toggle actions on any CallNode whose block is a
|
|
8
|
+
# BlockNode (i.e. not a bare proc/lambda literal).
|
|
9
|
+
#
|
|
10
|
+
# Emitted actions
|
|
11
|
+
# ───────────────
|
|
12
|
+
# 1. Convert to do…end
|
|
13
|
+
# receiver.method { |x| body }
|
|
14
|
+
# →
|
|
15
|
+
# receiver.method do |x|
|
|
16
|
+
# body
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# 2. Convert to brace block
|
|
20
|
+
# receiver.method do |x|
|
|
21
|
+
# body
|
|
22
|
+
# end
|
|
23
|
+
# →
|
|
24
|
+
# receiver.method { |x| body }
|
|
25
|
+
#
|
|
26
|
+
# Convention: multi-statement brace blocks are always expanded to do…end.
|
|
27
|
+
# Single-statement do…end blocks are collapsed to brace style.
|
|
28
|
+
class BlockStyleListener
|
|
29
|
+
include RubyLsp::Requests::Support::Common
|
|
30
|
+
include Support::NodeHelpers
|
|
31
|
+
|
|
32
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
33
|
+
@response_builder = response_builder
|
|
34
|
+
@node_context = node_context
|
|
35
|
+
|
|
36
|
+
dispatcher.register(self, :on_call_node_enter)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def on_call_node_enter(node)
|
|
40
|
+
return unless node_covers_cursor?(node)
|
|
41
|
+
|
|
42
|
+
block = node.block
|
|
43
|
+
return unless block.is_a?(Prism::BlockNode)
|
|
44
|
+
|
|
45
|
+
if brace_block?(block)
|
|
46
|
+
emit_to_do_end(node, block)
|
|
47
|
+
elsif single_statement_body?(block)
|
|
48
|
+
emit_to_brace(node, block)
|
|
49
|
+
end
|
|
50
|
+
rescue StandardError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def brace_block?(block)
|
|
57
|
+
block.opening_loc.slice == "{"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def single_statement_body?(block)
|
|
61
|
+
block.body&.body&.length == 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ── brace → do…end ──────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
def emit_to_do_end(call_node, block)
|
|
67
|
+
indent = indent_for(call_node)
|
|
68
|
+
params_src = params_string(block)
|
|
69
|
+
body_lines = block.body.body.map { |s| "#{indent} #{s.location.slice.strip}" }.join("\n")
|
|
70
|
+
|
|
71
|
+
new_block = " do#{params_src}\n#{body_lines}\n#{indent}end"
|
|
72
|
+
new_text = call_without_block(call_node) + new_block
|
|
73
|
+
|
|
74
|
+
@response_builder << Interface::CodeAction.new(
|
|
75
|
+
title: "Convert to do…end block",
|
|
76
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
77
|
+
edit: single_edit_workspace_edit(call_node, new_text)
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# ── do…end → brace ──────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
def emit_to_brace(call_node, block)
|
|
84
|
+
params_src = params_string(block)
|
|
85
|
+
body_src = block.body.body.first.location.slice.strip
|
|
86
|
+
|
|
87
|
+
new_block = " {#{params_src} #{body_src} }"
|
|
88
|
+
new_text = call_without_block(call_node) + new_block
|
|
89
|
+
|
|
90
|
+
@response_builder << Interface::CodeAction.new(
|
|
91
|
+
title: "Convert to brace block",
|
|
92
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
93
|
+
edit: single_edit_workspace_edit(call_node, new_text)
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ── helpers ──────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
# Returns the block parameters string including pipes, e.g. " |x, y|",
|
|
100
|
+
# or an empty string when the block takes no parameters.
|
|
101
|
+
def params_string(block)
|
|
102
|
+
return "" unless block.parameters
|
|
103
|
+
|
|
104
|
+
" #{block.parameters.location.slice}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns the source of the call node up to (but not including) the block.
|
|
108
|
+
# Works by slicing from the call's start to the block's start.
|
|
109
|
+
def call_without_block(call_node)
|
|
110
|
+
call_src = call_node.location.slice
|
|
111
|
+
block_src = call_node.block.location.slice
|
|
112
|
+
# Remove the block suffix (and any whitespace before it) from the call.
|
|
113
|
+
call_src[0, call_src.length - block_src.length].rstrip
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -127,8 +127,8 @@ module RubyLsp
|
|
|
127
127
|
|
|
128
128
|
@response_builder << Interface::CodeAction.new(
|
|
129
129
|
title: "Convert to post-conditional",
|
|
130
|
-
kind:
|
|
131
|
-
edit:
|
|
130
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
131
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
132
132
|
)
|
|
133
133
|
end
|
|
134
134
|
|
|
@@ -141,8 +141,8 @@ module RubyLsp
|
|
|
141
141
|
|
|
142
142
|
@response_builder << Interface::CodeAction.new(
|
|
143
143
|
title: "Convert to post-conditional",
|
|
144
|
-
kind:
|
|
145
|
-
edit:
|
|
144
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
145
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
146
146
|
)
|
|
147
147
|
end
|
|
148
148
|
|
|
@@ -155,8 +155,8 @@ module RubyLsp
|
|
|
155
155
|
|
|
156
156
|
@response_builder << Interface::CodeAction.new(
|
|
157
157
|
title: "Convert to block if",
|
|
158
|
-
kind:
|
|
159
|
-
edit:
|
|
158
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
159
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
160
160
|
)
|
|
161
161
|
end
|
|
162
162
|
|
|
@@ -169,8 +169,8 @@ module RubyLsp
|
|
|
169
169
|
|
|
170
170
|
@response_builder << Interface::CodeAction.new(
|
|
171
171
|
title: "Convert to block unless",
|
|
172
|
-
kind:
|
|
173
|
-
edit:
|
|
172
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
173
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
174
174
|
)
|
|
175
175
|
end
|
|
176
176
|
|
|
@@ -183,8 +183,8 @@ module RubyLsp
|
|
|
183
183
|
|
|
184
184
|
@response_builder << Interface::CodeAction.new(
|
|
185
185
|
title: "Convert to unless",
|
|
186
|
-
kind:
|
|
187
|
-
edit:
|
|
186
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
187
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
188
188
|
)
|
|
189
189
|
end
|
|
190
190
|
|
|
@@ -197,8 +197,8 @@ module RubyLsp
|
|
|
197
197
|
|
|
198
198
|
@response_builder << Interface::CodeAction.new(
|
|
199
199
|
title: "Convert to if",
|
|
200
|
-
kind:
|
|
201
|
-
edit:
|
|
200
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
201
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
202
202
|
)
|
|
203
203
|
end
|
|
204
204
|
|
|
@@ -212,8 +212,8 @@ module RubyLsp
|
|
|
212
212
|
|
|
213
213
|
@response_builder << Interface::CodeAction.new(
|
|
214
214
|
title: "Invert if/else",
|
|
215
|
-
kind:
|
|
216
|
-
edit:
|
|
215
|
+
kind: Constant::CodeActionKind::REFACTOR_REWRITE,
|
|
216
|
+
edit: single_edit_workspace_edit(node, new_text)
|
|
217
217
|
)
|
|
218
218
|
end
|
|
219
219
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/node_helpers"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module Refactor
|
|
7
|
+
# Offers "Extract constant" when the cursor is on a literal value
|
|
8
|
+
# (Integer, Float, String, Symbol) inside a class or module body.
|
|
9
|
+
#
|
|
10
|
+
# The new constant is inserted at the top of the enclosing class/module
|
|
11
|
+
# body, and the literal is replaced with the constant name.
|
|
12
|
+
#
|
|
13
|
+
# Input (cursor on `100`):
|
|
14
|
+
# class Processor
|
|
15
|
+
# def run
|
|
16
|
+
# items.first(100)
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Output:
|
|
21
|
+
# class Processor
|
|
22
|
+
# EXTRACTED_CONSTANT = 100
|
|
23
|
+
#
|
|
24
|
+
# def run
|
|
25
|
+
# items.first(EXTRACTED_CONSTANT)
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
class ConstantListener
|
|
29
|
+
include RubyLsp::Requests::Support::Common
|
|
30
|
+
include Support::NodeHelpers
|
|
31
|
+
|
|
32
|
+
LITERAL_NODE_TYPES = [
|
|
33
|
+
Prism::IntegerNode,
|
|
34
|
+
Prism::FloatNode,
|
|
35
|
+
Prism::StringNode,
|
|
36
|
+
Prism::SymbolNode
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
def initialize(response_builder, node_context, dispatcher)
|
|
40
|
+
@response_builder = response_builder
|
|
41
|
+
@node_context = node_context
|
|
42
|
+
|
|
43
|
+
# Track the nearest enclosing class/module so we know where to insert.
|
|
44
|
+
@class_stack = []
|
|
45
|
+
|
|
46
|
+
dispatcher.register(
|
|
47
|
+
self,
|
|
48
|
+
:on_class_node_enter,
|
|
49
|
+
:on_class_node_leave,
|
|
50
|
+
:on_module_node_enter,
|
|
51
|
+
:on_module_node_leave,
|
|
52
|
+
:on_integer_node_enter,
|
|
53
|
+
:on_float_node_enter,
|
|
54
|
+
:on_string_node_enter,
|
|
55
|
+
:on_symbol_node_enter
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def on_class_node_enter(node) = @class_stack.push(node)
|
|
60
|
+
def on_class_node_leave(_node) = @class_stack.pop
|
|
61
|
+
def on_module_node_enter(node) = @class_stack.push(node)
|
|
62
|
+
def on_module_node_leave(_node) = @class_stack.pop
|
|
63
|
+
|
|
64
|
+
def on_integer_node_enter(node) = maybe_emit(node)
|
|
65
|
+
def on_float_node_enter(node) = maybe_emit(node)
|
|
66
|
+
def on_string_node_enter(node) = maybe_emit(node)
|
|
67
|
+
def on_symbol_node_enter(node) = maybe_emit(node)
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def maybe_emit(node)
|
|
72
|
+
return unless node_covers_cursor?(node)
|
|
73
|
+
return if @class_stack.empty?
|
|
74
|
+
|
|
75
|
+
# Don't offer on constant assignments themselves.
|
|
76
|
+
enclosing = @class_stack.last
|
|
77
|
+
return unless enclosing
|
|
78
|
+
|
|
79
|
+
emit_extract_constant(node, enclosing)
|
|
80
|
+
rescue StandardError
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def emit_extract_constant(literal_node, class_node)
|
|
85
|
+
literal_src = literal_node.location.slice.strip
|
|
86
|
+
indent = indent_for(class_node)
|
|
87
|
+
body_indent = "#{indent} "
|
|
88
|
+
|
|
89
|
+
# Insert the constant declaration at the top of the class body.
|
|
90
|
+
# The class body starts on the line after the class declaration.
|
|
91
|
+
body_start_line = class_node.body&.location&.start_line
|
|
92
|
+
return unless body_start_line
|
|
93
|
+
|
|
94
|
+
insert_line = body_start_line - 1
|
|
95
|
+
const_decl = "#{body_indent}EXTRACTED_CONSTANT = #{literal_src}\n\n"
|
|
96
|
+
|
|
97
|
+
insert_edit = Interface::TextEdit.new(
|
|
98
|
+
range: Interface::Range.new(
|
|
99
|
+
start: Interface::Position.new(line: insert_line, character: 0),
|
|
100
|
+
end: Interface::Position.new(line: insert_line, character: 0)
|
|
101
|
+
),
|
|
102
|
+
new_text: const_decl
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
replace_edit = Interface::TextEdit.new(
|
|
106
|
+
range: node_to_lsp_range(literal_node),
|
|
107
|
+
new_text: "EXTRACTED_CONSTANT"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@response_builder << Interface::CodeAction.new(
|
|
111
|
+
title: "Extract constant",
|
|
112
|
+
kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
|
|
113
|
+
edit: multi_edit_workspace_edit([insert_edit, replace_edit])
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|