ruby-lsp-refactor
A ruby-lsp add-on that provides safe, AST-driven refactoring code actions natively inside any LSP-supported editor (VS Code, Zed, Neovim, RubyMine, etc.).
All refactors are powered by the Prism parser and operate on the real AST — no regex substitutions.
Installation
Add the gem to your project's Gemfile (it only needs to be available to the
language server, so the :development group is the right place):
group :development do
gem "ruby-lsp-refactor"
end
Then run:
bundle install
The add-on is discovered and activated automatically by ruby-lsp — no further configuration is required.
Supported refactorings
Place your cursor anywhere on the relevant construct and open the code-actions
menu (Cmd+. in VS Code / Zed, or your editor's equivalent).
Phase 1 — Local rewrites
Convert to post-conditional
Collapses a single-statement if or unless block into a trailing modifier.
# Before
if user.qualified?
user.approve!
end
# After
user.approve! if user.qualified?
Works with unless too:
# Before
unless user.banned?
user.login!
end
# After
user.login! unless user.banned?
Convert to block if / Convert to block unless
The reverse operation — expands a trailing modifier back into a full block.
# Before
user.approve! if user.qualified?
# After
if user.qualified?
user.approve!
end
Convert to unless / Convert to if
Toggles between if and unless on a block conditional that has no else
branch. When the predicate already starts with !, the negation is stripped
automatically to keep the result clean.
# Before
if user.active?
user.greet!
end
# After
unless user.active?
user.greet!
end
# Before — negated predicate
if !user.banned?
user.login!
end
# After — negation stripped
unless user.banned?
user.login!
end
Invert if/else
Negates the condition and swaps the two branches of an if/else block.
Double-negation (!!) is cancelled automatically.
# Before
if user.admin?
grant!
else
deny!
end
# After
if !user.admin?
deny!
else
grant!
end
Convert to interpolated string
Upgrades a single-quoted string literal to double-quotes so you can immediately
add #{} interpolation. Any " characters inside the string are escaped.
# Before
'hello world'
# After
"hello world"
Phase 2 — Variable & literal optimisation
Inline variable
Removes a local variable assignment and replaces every subsequent read of that variable with the original right-hand-side expression.
# Before — cursor on the assignment line
result = user.calculate
puts result
log result
# After
puts user.calculate
log user.calculate
Extract local variable
Wraps any expression under the cursor in a new local variable assignment inserted on the line above.
# Before — cursor on the expression
user.full_name.upcase
# After
variable = user.full_name.upcase
variable
Convert to keyword syntax
Converts hash-rocket pairs whose keys are plain symbols into modern keyword syntax. Mixed hashes (string keys, computed keys) are handled gracefully — only the eligible pairs are converted.
# Before
{ :name => "Alice", :age => 30 }
# After
{ name: "Alice", age: 30 }
Convert to symbol array
Converts a bracket array of plain symbols into a %i[] word array.
# Before
[:foo, :bar, :baz]
# After
%i[foo bar baz]
Phase 3 — Advanced structure
Extract to method
Extracts a local variable's right-hand-side expression into a new private
method. Variables that are defined before the extraction point and referenced
inside the expression are automatically detected and forwarded as method
parameters.
# Before — cursor on the assignment
def process(data)
threshold = 10
result = data.select { |x| x > threshold }
result
end
# After
def process(data)
threshold = 10
result = result(threshold)
result
end
private
def result(threshold)
data.select { |x| x > threshold }
end
Add parameter
Appends a new_param placeholder to a method's parameter list. If the method
has no parameters yet, parentheses are added automatically.
# Before — cursor anywhere inside the def
def greet(name)
puts name
end
# After
def greet(name, new_param)
puts name
end
# Before — no parameters
def greet
puts "hello"
end
# After
def greet(new_param)
puts "hello"
end
Convert to keyword arguments
Rewrites all required positional parameters in a method signature to keyword arguments. Optional parameters, rest args, and block parameters are left unchanged.
# Before — cursor anywhere inside the def
def create(name, age)
User.new(name, age)
end
# After
def create(name:, age:)
User.new(name, age)
end
Extract to let (RSpec)
When the cursor is on a local variable assignment inside an RSpec it,
specify, example, or scenario block, this action moves the assignment
into a let declaration inserted above the example.
# Before — cursor on the assignment
it "logs in" do
user = User.new(name: "Alice")
expect(user.name).to eq("Alice")
end
# After
let(:user) { User.new(name: "Alice") }
it "logs in" do
expect(user.name).to eq("Alice")
end
Development
bin/setup # install dependencies
bundle exec rake test # run the test suite
bundle exec rake # lint + test
To try the add-on against a local project without publishing to RubyGems, add
a path reference to that project's Gemfile:
gem "ruby-lsp-refactor", path: "/path/to/ruby-lsp-refactor"
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/tachyons/ruby-lsp-refactor.
License
The gem is available as open source under the terms of the MIT License.