{"id":51472465,"url":"https://github.com/tachyons/ruby-lsp-refactor","last_synced_at":"2026-07-06T19:01:27.335Z","repository":{"id":365044165,"uuid":"1270298753","full_name":"tachyons/ruby-lsp-refactor","owner":"tachyons","description":"ruby-lsp plugin to perform common refactorings as code action","archived":false,"fork":false,"pushed_at":"2026-06-15T16:17:35.000Z","size":50,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-15T17:14:52.804Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tachyons.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-15T15:18:55.000Z","updated_at":"2026-06-15T16:59:00.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/tachyons/ruby-lsp-refactor","commit_stats":null,"previous_names":["tachyons/ruby-lsp-refactor"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/tachyons/ruby-lsp-refactor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tachyons%2Fruby-lsp-refactor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tachyons%2Fruby-lsp-refactor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tachyons%2Fruby-lsp-refactor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tachyons%2Fruby-lsp-refactor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tachyons","download_url":"https://codeload.github.com/tachyons/ruby-lsp-refactor/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tachyons%2Fruby-lsp-refactor/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35202786,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-06T02:00:07.184Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-06T19:01:25.718Z","updated_at":"2026-07-06T19:01:27.304Z","avatar_url":"https://github.com/tachyons.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ruby-lsp-refactor\n\n\u003e **Beta software.** This gem is under active development. Refactorings may\n\u003e produce incorrect output in edge cases. A significant portion of the\n\u003e implementation was written with AI assistance — please review generated edits\n\u003e before committing them. Bug reports and corrections are very welcome.\n\nA [ruby-lsp](https://github.com/Shopify/ruby-lsp) add-on that provides\nAST-driven refactoring code actions natively inside any LSP-supported editor\n(VS Code, Zed, Neovim, RubyMine, etc.).\n\nAll refactors are powered by the [Prism](https://github.com/ruby/prism) parser\nand operate on the real AST — no regex substitutions.\n\n## Installation\n\nAdd the gem to your project's `Gemfile` (it only needs to be available to the\nlanguage server, so the `:development` group is the right place):\n\n```ruby\ngroup :development do\n  gem \"ruby-lsp-refactor\"\nend\n```\n\nThen run:\n\n```bash\nbundle install\n```\n\nThe add-on is discovered and activated automatically by ruby-lsp — no further\nconfiguration is required.\n\n\u003e **Note on upstream overlap.** ruby-lsp already provides \"Refactor: Extract\n\u003e Variable\", \"Refactor: Extract Method\", and \"Refactor: Toggle block style\"\n\u003e natively. This add-on intentionally does not duplicate those actions — place\n\u003e your cursor on any expression or block and they will appear alongside the\n\u003e refactorings listed below.\n\n## Supported refactorings\n\nPlace your cursor anywhere on the relevant construct and open the code-actions\nmenu (`Cmd+.` in VS Code / Zed, or your editor's equivalent).\n\n### Conditionals\n\n#### Convert to post-conditional\n\nCollapses a single-statement `if` or `unless` block into a trailing modifier.\n\n```ruby\n# Before\nif user.qualified?\n  user.approve!\nend\n\n# After\nuser.approve! if user.qualified?\n```\n\n#### Convert to block if / Convert to block unless\n\nThe reverse — expands a trailing modifier back into a full block.\n\n```ruby\n# Before\nuser.approve! if user.qualified?\n\n# After\nif user.qualified?\n  user.approve!\nend\n```\n\n#### Convert to unless / Convert to if\n\nToggles between `if` and `unless` on a block conditional with no `else` branch.\nWhen the predicate already starts with `!`, the negation is stripped\nautomatically.\n\n```ruby\n# Before\nif !user.banned?\n  user.login!\nend\n\n# After — negation stripped\nunless user.banned?\n  user.login!\nend\n```\n\n#### Invert if/else\n\nNegates the condition and swaps the two branches. Double-negation (`!!`) is\ncancelled automatically.\n\n```ruby\n# Before\nif user.admin?\n  grant!\nelse\n  deny!\nend\n\n# After\nif !user.admin?\n  deny!\nelse\n  grant!\nend\n```\n\n#### Convert to early return\n\nConverts a guard `if` block at the top of a method into a `return unless`\nstatement, eliminating unnecessary nesting. The method body must have no\n`else` branch and the `if` must be the first statement.\n\n```ruby\n# Before — cursor on the if\ndef charge_purchase(order)\n  if order.fulfilled?\n    OrderChargeConfirmation.new(order).create!\n  end\nend\n\n# After\ndef charge_purchase(order)\n  return unless order.fulfilled?\n  OrderChargeConfirmation.new(order).create!\nend\n```\n\n---\n\n### Strings\n\n#### Convert to interpolated string\n\nUpgrades a single-quoted string to double-quotes so you can immediately add\n`#{}` interpolation. Embedded `\"` characters are escaped.\n\n```ruby\n'hello world'  →  \"hello world\"\n```\n\n#### Convert to string array / Convert to bracket array\n\nConverts between a bracket array of plain strings and `%w[]` syntax.\n\n```ruby\n[\"foo\", \"bar\", \"baz\"]  →  %w[foo bar baz]\n%w[foo bar baz]        →  [\"foo\", \"bar\", \"baz\"]\n```\n\n#### Wrap in freeze / Remove freeze\n\nAdds or removes `.freeze` on a string literal.\n\n```ruby\n\"hello\"          →  \"hello\".freeze\n\"hello\".freeze   →  \"hello\"\n```\n\n---\n\n### Collections\n\n#### Convert to symbol array\n\nConverts a bracket array of plain symbols into a `%i[]` word array.\n\n```ruby\n[:foo, :bar, :baz]  →  %i[foo bar baz]\n```\n\n#### Convert to keyword syntax\n\nConverts hash-rocket pairs whose keys are plain symbols into modern keyword\nsyntax. Mixed hashes are handled gracefully — only eligible pairs are\nconverted.\n\n```ruby\n{ :name =\u003e \"Alice\", :age =\u003e 30 }  →  { name: \"Alice\", age: 30 }\n```\n\n#### Convert to .flat_map\n\nCollapses a `map` + `flatten` / `flatten(1)` chain.\n\n```ruby\nitems.map { |i| i.tags }.flatten(1)  →  items.flat_map { |i| i.tags }\n```\n\n#### Convert to .find\n\nCollapses a `select` + `first` chain.\n\n```ruby\nusers.select { |u| u.admin? }.first  →  users.find { |u| u.admin? }\n```\n\n#### Convert to .filter_map\n\nCollapses a `map` + `compact` chain.\n\n```ruby\nitems.map { |i| i.value }.compact  →  items.filter_map { |i| i.value }\n```\n\n---\n\n### Variables \u0026 constants\n\n#### Inline variable\n\nRemoves a local variable assignment and replaces every subsequent read with the\noriginal right-hand-side expression.\n\n```ruby\n# Before — cursor on the assignment\nresult = user.calculate\nputs result\nlog result\n\n# After\nputs user.calculate\nlog user.calculate\n```\n\n#### Extract constant\n\nExtracts a literal value (integer, float, string, symbol) inside a class or\nmodule into a named constant at the top of the enclosing body.\n\n```ruby\n# Before — cursor on 100\nclass Processor\n  def run\n    items.first(100)\n  end\nend\n\n# After\nclass Processor\n  EXTRACTED_CONSTANT = 100\n\n  def run\n    items.first(EXTRACTED_CONSTANT)\n  end\nend\n```\n\n---\n\n### Methods \u0026 classes\n\n#### Add parameter\n\nAppends a `new_param` placeholder to a method's parameter list. Parentheses\nare added automatically when the method has none.\n\n```ruby\ndef greet(name)  →  def greet(name, new_param)\ndef greet        →  def greet(new_param)\n```\n\n#### Convert to keyword arguments\n\nRewrites required positional parameters to keyword arguments. Optional\nparameters, rest args, and block parameters are left unchanged.\n\n```ruby\ndef create(name, age)  →  def create(name:, age:)\n```\n\n#### Convert to attr_accessor\n\nDetects an `attr_reader` paired with a canonical manual writer\n(`def name=(val); @name = val; end`) and collapses them into a single\n`attr_accessor`.\n\n```ruby\n# Before — cursor on either line\nattr_reader :name\ndef name=(val)\n  @name = val\nend\n\n# After\nattr_accessor :name\n```\n\n#### Wrap body in rescue\n\nWraps a method's entire body in a `rescue StandardError =\u003e e` clause with a\n`raise` placeholder so you can fill in the error handling without accidentally\nswallowing exceptions.\n\n```ruby\n# Before\ndef call\n  do_thing\nend\n\n# After\ndef call\n  do_thing\nrescue StandardError =\u003e e\n  raise\nend\n```\n\n#### Extract predicate methods\n\nExtracts each operand of a compound `\u0026\u0026` or `||` expression that is the sole\nstatement in a method into its own private predicate method. The generated\nnames `predicate_1?` / `predicate_2?` are placeholders — rename them to\nreflect intent.\n\n```ruby\n# Before — cursor on the compound expression\ndef eligible_for_return?\n  expired_orders.exclude?(self) \u0026\u0026 self.value \u003e MINIMUM_RETURN_VALUE\nend\n\n# After\ndef eligible_for_return?\n  predicate_1? \u0026\u0026 predicate_2?\nend\n\nprivate\n\ndef predicate_1?\n  expired_orders.exclude?(self)\nend\n\ndef predicate_2?\n  self.value \u003e MINIMUM_RETURN_VALUE\nend\n```\n\n#### Convert to explicit super\n\nConverts a bare `super` (which forwards all arguments implicitly) into an\nexplicit `super(param1, param2, ...)` using the enclosing method's parameter\nnames.\n\n```ruby\ndef initialize(name, age)\n  super          →  super(name, age)\nend\n```\n\n---\n\n### Operators \u0026 blocks\n\n#### Convert to tap\n\nConverts a sequence of method calls on the same receiver followed by a bare\nreturn of that receiver into an `Object#tap` block, grouping the operations\nand removing the explicit return.\n\n```ruby\n# Before — cursor anywhere in the method\ndef do_something\n  obj.do_first_thing\n  obj.do_second_thing\n  obj.do_third_thing\n  obj\nend\n\n# After\ndef do_something\n  obj.tap do |o|\n    o.do_first_thing\n    o.do_second_thing\n    o.do_third_thing\n  end\nend\n```\n\n#### Convert `\u0026\u0026` to `and` / `and` to `\u0026\u0026`\n\nToggles between symbolic and word forms of the logical AND operator.\n\n```ruby\nuser.valid? \u0026\u0026 user.save  →  user.valid? and user.save\n```\n\n#### Convert `||` to `or` / `or` to `||`\n\nToggles between symbolic and word forms of the logical OR operator.\n\n```ruby\na || b  →  a or b\n```\n\n#### Simplify raise\n\nRemoves the redundant `RuntimeError` class from a two-argument `raise` or\n`fail` call. `RuntimeError` is Ruby's default exception class and need not be\nstated explicitly.\n\n```ruby\nraise RuntimeError, \"oops\"  →  raise \"oops\"\n```\n\n---\n\n### RSpec\n\n#### Extract to let\n\nMoves a local variable assignment inside an `it`/`specify`/`example`/`scenario`\nblock into a `let` declaration above the example.\n\n```ruby\n# Before — cursor on the assignment\nit \"logs in\" do\n  user = User.new(name: \"Alice\")\n  expect(user.name).to eq(\"Alice\")\nend\n\n# After\nlet(:user) { User.new(name: \"Alice\") }\n\nit \"logs in\" do\n  expect(user.name).to eq(\"Alice\")\nend\n```\n\n#### Convert let to let! / let! to let\n\nToggles between lazy (`let`) and eager (`let!`) memoization.\n\n```ruby\nlet(:user) { User.new }   →  let!(:user) { User.new }\nlet!(:user) { User.new }  →  let(:user) { User.new }\n```\n\n---\n\n## Planned refactorings\n\nThe items below are on the roadmap but not yet implemented. They are tracked\nhere so the intent is not lost.\n\n### Single-file (not yet implemented)\n\n| Refactoring | Description |\n|---|---|\n| **Introduce field** | Extracts an expression inside a method into an instance variable (`@name`), inserting the assignment at the top of the method or into `initialize`. |\n\n### Multi-file\n\nThese refactorings create new files and/or update call sites across the\nproject. The ones marked ✅ are already implemented using `document_changes`\nin the `WorkspaceEdit` response, which lets a single code action atomically\ncreate files and edit multiple documents. The ones marked 🔲 require\nworkspace-level index support or are pending implementation.\n\n#### ✅ Extract Include File\n\nExtracts a top-level `module` or `class` into its own file and replaces it\nwith a `require_relative` statement. Offered when the cursor is on a module or\nclass that coexists with other top-level statements in the same file.\n\n```ruby\n# Before — app/models/user.rb (cursor on the module)\nmodule Greetable\n  def greet = \"hello\"\nend\n\nclass User\n  include Greetable\nend\n\n# After — app/models/greetable.rb (new file, created automatically)\n# frozen_string_literal: true\n\nmodule Greetable\n  def greet = \"hello\"\nend\n\n# After — app/models/user.rb (modified)\nrequire_relative \"greetable\"\n\nclass User\n  include Greetable\nend\n```\n\n#### 🔲 Extract Service Object\n\nMoves callback logic out of a controller into a dedicated service object file.\nAddresses the Rails antipattern of using `after_action` callbacks for\noperations that depend on the success of the triggering action.\n\n```ruby\n# Before — app/controllers/users_controller.rb\nclass UsersController \u003c ApplicationController\n  after_action :send_confirmation_email, only: [:create]\n\n  def create\n    @user = User.create!(user_params)\n  end\nend\n\n# After — app/services/user_confirmation_service.rb (new file)\nclass UserConfirmationService\n  def initialize(user) = @user = user\n\n  def call\n    AccountCreationMailer.new(@user).deliver! if @user.persisted?\n  end\nend\n\n# After — app/controllers/users_controller.rb (modified)\nclass UsersController \u003c ApplicationController\n  def create\n    @user = User.create!(user_params)\n    UserConfirmationService.new(@user).call\n  end\nend\n```\n\n#### 🔲 Extract Form Object\n\nExtracts a model that uses `accepts_nested_attributes_for` into a plain Ruby\nform object that includes `ActiveModel::Model`, making the form flat,\nexplicitly validated, and easy to test.\n\nCreates a new file under `app/forms/` and updates the controller and view to\nuse the form object instead of the model directly.\n\n#### 🔲 Extract Policy Class\n\nWhen a method contains more than two or three compound conditions, extracts\nthem all into a dedicated policy class with individual predicate methods. Each\npredicate becomes a public method on the policy, making them independently\ntestable without stubs.\n\n```ruby\n# Before — single method with many conditions\ndef eligible_for_return?\n  not_expired? \u0026\u0026 over_minimum_value? \u0026\u0026 customer_not_fraudulent?\nend\n\n# After — app/policies/return_eligibility_policy.rb (new file)\nclass ReturnEligibilityPolicy\n  def initialize(order) = @order = order\n\n  def eligible?\n    not_expired? \u0026\u0026 over_minimum_value? \u0026\u0026 customer_not_fraudulent?\n  end\n\n  def not_expired?             = Order.expired_orders.exclude?(@order)\n  def over_minimum_value?      = @order.value \u003e Order::MINIMUM_RETURN_VALUE\n  def customer_not_fraudulent? = @order.user.not_fraudulent?\nend\n\n# After — calling code (modified)\ndef eligible_for_return?\n  ReturnEligibilityPolicy.new(self).eligible?\nend\n```\n\n#### 🔲 Combine Functions into Class\n\nWhen several methods in a file all take the same object as their first\nargument, extracts them into a new class where that object becomes an injected\ndependency. Implements the\n[Combine Functions into Class](https://refactoring.com/catalog/combineFunctionsIntoClass.html)\npattern.\n\n```ruby\n# Before — repeated argument is a smell\ndef format_name(user) = \"#{user.first_name} #{user.last_name}\"\ndef greeting(user)    = \"Hello, #{format_name(user)}\"\ndef farewell(user)    = \"Goodbye, #{format_name(user)}\"\n\n# After — app/presenters/user_presenter.rb (new file)\nclass UserPresenter\n  def initialize(user) = @user = user\n\n  def format_name = \"#{@user.first_name} #{@user.last_name}\"\n  def greeting    = \"Hello, #{format_name}\"\n  def farewell    = \"Goodbye, #{format_name}\"\nend\n```\n\n#### 🔲 Introduce Null Object\n\nWhen a method guards against a nil association with an `if` check before\ndelegating to it, extracts a null object class that implements the same\ninterface with safe default behaviour, removing the conditional entirely.\n\n```ruby\n# Before\nif @user.has_address?\n  @user.address.street_name\nelse\n  \"Unknown street\"\nend\n\n# After — app/models/null_address.rb (new file)\nclass NullAddress\n  def street_name = \"Unknown street\"\nend\n\n# After — app/models/user.rb (modified)\nclass User\n  def address = @address || NullAddress.new\nend\n\n# After — calling code (no conditional needed)\n@user.address.street_name\n```\n\n#### 🔲 Rename\n\nRenames a method, class, module, constant, or local variable and updates\nevery reference to it across the entire project. Requires the ruby-lsp index\nto locate all usages safely.\n\n#### 🔲 Extract Parameter\n\nExtracts an expression inside a method body into a new parameter, adding it\nto the method signature and updating every call site in the project to pass\nthe extracted value.\n\n```ruby\n# Before\ndef greet\n  \"Hello, #{DEFAULT_NAME}\"\nend\n\n# After — signature and all call sites updated\ndef greet(name = DEFAULT_NAME)\n  \"Hello, #{name}\"\nend\n```\n\n#### 🔲 Extract Superclass\n\nExtracts selected methods from a class into a new superclass and makes the\noriginal class inherit from it. Creates a new file for the superclass.\n\n```ruby\n# Before — app/models/animal.rb\nclass Animal\n  def breathe = \"breathing\"\n  def eat      = \"eating\"\n  def speak    = raise NotImplementedError\nend\n\n# After — app/models/living_thing.rb (new file)\nclass LivingThing\n  def breathe = \"breathing\"\n  def eat      = \"eating\"\nend\n\n# After — app/models/animal.rb (modified)\nclass Animal \u003c LivingThing\n  def speak = raise NotImplementedError\nend\n```\n\n#### 🔲 Extract Module\n\nExtracts selected methods from a class into a new module and adds an\n`include` statement. Creates a new file for the module.\n\n```ruby\n# Before\nclass Report\n  def format_header = \"=== Report ===\"\n  def format_footer = \"=== End ===\"\n  def generate      = \"#{format_header}\\n...\\n#{format_footer}\"\nend\n\n# After — app/concerns/formattable.rb (new file)\nmodule Formattable\n  def format_header = \"=== Report ===\"\n  def format_footer = \"=== End ===\"\nend\n\n# After — app/models/report.rb (modified)\nclass Report\n  include Formattable\n  def generate = \"#{format_header}\\n...\\n#{format_footer}\"\nend\n```\n\n#### 🔲 Pull Members Up / Push Members Down\n\nMoves methods between a class and its superclass. \"Pull up\" moves a method\nfrom a subclass to the superclass; \"push down\" moves it from the superclass\ninto one or more subclasses. Both operations update all affected files.\n\n#### 🔲 Safe Delete\n\nDeletes a method, class, or constant only after verifying it has no usages\nanywhere in the project. Requires the ruby-lsp index to confirm the symbol is\nunreferenced before removing it.\n\n#### 🔲 Extract Partial _(Rails)_\n\nExtracts a fragment of an ERB view template into a new partial file and\nreplaces the original fragment with a `render` call.\n\n```erb\n\u003c%# Before — app/views/users/show.html.erb %\u003e\n\u003cdiv class=\"profile\"\u003e\n  \u003ch1\u003e\u003c%= @user.name %\u003e\u003c/h1\u003e\n  \u003cp\u003e\u003c%= @user.bio %\u003e\u003c/p\u003e\n\u003c/div\u003e\n\n\u003c%# After — app/views/users/_profile.html.erb (new file) %\u003e\n\u003cdiv class=\"profile\"\u003e\n  \u003ch1\u003e\u003c%= user.name %\u003e\u003c/h1\u003e\n  \u003cp\u003e\u003c%= user.bio %\u003e\u003c/p\u003e\n\u003c/div\u003e\n\n\u003c%# After — app/views/users/show.html.erb (modified) %\u003e\n\u003c%= render \"profile\", user: @user %\u003e\n```\n\n#### 🔲 Extract Include File (generic)\n\nExtracts an arbitrary block of Ruby code (not necessarily a named module or\nclass) into a new file and replaces it with a `require_relative` statement.\nThe ✅ variant above handles the named module/class case automatically; this\ngeneric form would handle any selected lines.\n\n---\n\n## Development\n\n```bash\nbin/setup             # install dependencies\nbundle exec rake test # run the test suite\nbundle exec rake      # lint + test\n```\n\nTo try the add-on against a local project without publishing to RubyGems, add\na path reference to that project's `Gemfile`:\n\n```ruby\ngem \"ruby-lsp-refactor\", path: \"/path/to/ruby-lsp-refactor\"\n```\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at\nhttps://github.com/tachyons/ruby-lsp-refactor.\n\n## License\n\nThe gem is available as open source under the terms of the\n[MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftachyons%2Fruby-lsp-refactor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftachyons%2Fruby-lsp-refactor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftachyons%2Fruby-lsp-refactor/lists"}