diff --git a/lib/ruby_lsp/ruby_lsp_rails/addon.rb b/lib/ruby_lsp/ruby_lsp_rails/addon.rb index 63aa0501..de1bf3af 100644 --- a/lib/ruby_lsp/ruby_lsp_rails/addon.rb +++ b/lib/ruby_lsp/ruby_lsp_rails/addon.rb @@ -16,6 +16,7 @@ require_relative "definition" require_relative "rails_test_style" require_relative "completion" +require_relative "rename" require_relative "indexing_enhancement" module RubyLsp @@ -138,6 +139,14 @@ def create_completion_listener(response_builder, node_context, dispatcher, uri) Completion.new(@rails_runner_client, response_builder, node_context, dispatcher, uri) end + # @override + #: (String fully_qualified_name, String new_name, Array[(Interface::RenameFile | Interface::TextDocumentEdit)] document_changes) -> void + def collect_file_renames(fully_qualified_name, new_name, document_changes) + return unless @global_state + + Rename.new(@global_state.index, fully_qualified_name, new_name, document_changes) + end + #: (Array[{uri: String, type: Integer}] changes) -> void def workspace_did_change_watched_files(changes) if changes.any? { |c| c[:uri].end_with?("db/schema.rb") || c[:uri].end_with?("structure.sql") } diff --git a/lib/ruby_lsp/ruby_lsp_rails/rename.rb b/lib/ruby_lsp/ruby_lsp_rails/rename.rb new file mode 100644 index 00000000..26f5d5d0 --- /dev/null +++ b/lib/ruby_lsp/ruby_lsp_rails/rename.rb @@ -0,0 +1,41 @@ +# typed: strict +# frozen_string_literal: true + +module RubyLsp + module Rails + class Rename + #: (RubyIndexer::Index index, String fully_qualified_name, String new_name, Array[(Interface::RenameFile | Interface::TextDocumentEdit)] document_changes) -> void + def initialize(index, fully_qualified_name, new_name, document_changes) + index[fully_qualified_name]&.each do |entry| + file_path = entry.uri.full_path + next unless file_path + + next unless file_path.include?("/db/migrate/") + + old_file_name = entry.file_name + old_snake = file_from_constant_name(entry.name.split("::").last) + + next unless old_file_name.match?(/\A\d+_#{Regexp.escape(old_snake)}\.rb\z/) + + timestamp_prefix = old_file_name.delete_suffix("_#{old_snake}.rb") + new_snake = file_from_constant_name(new_name.split("::").last) + + new_uri = URI::Generic.from_path( + path: File.join(File.dirname(file_path), "#{timestamp_prefix}_#{new_snake}.rb"), + ).to_s + + document_changes << Interface::RenameFile.new(kind: "rename", old_uri: entry.uri.to_s, new_uri: new_uri) + end + end + + private + + #: (String constant_name) -> String + def file_from_constant_name(constant_name) + constant_name + .gsub(/([a-z])([A-Z])|([A-Z])([A-Z][a-z])/, '\1\3_\2\4') + .downcase + end + end + end +end diff --git a/test/ruby_lsp_rails/rename_test.rb b/test/ruby_lsp_rails/rename_test.rb new file mode 100644 index 00000000..34121dd9 --- /dev/null +++ b/test/ruby_lsp_rails/rename_test.rb @@ -0,0 +1,65 @@ +# typed: true +# frozen_string_literal: true + +require "test_helper" + +module RubyLsp + module Rails + class RenameTest < ActiveSupport::TestCase + test "renames migration file to match new class name" do + document_changes = collect_file_renames( + "#{dummy_root}/db/migrate/20210901000000_create_foos.rb", + "class CreateFoos < ActiveRecord::Migration[7.0]; end", + "CreateFoos", + "CreateBars", + ) + + assert_equal(1, document_changes.size) + rename = document_changes.first + assert_instance_of(Interface::RenameFile, rename) + assert_equal( + URI::Generic.from_path(path: "#{dummy_root}/db/migrate/20210901000000_create_foos.rb").to_s, + rename.old_uri, + ) + assert_equal( + URI::Generic.from_path(path: "#{dummy_root}/db/migrate/20210901000000_create_bars.rb").to_s, + rename.new_uri, + ) + end + + test "does nothing for non-migration files" do + document_changes = collect_file_renames( + "#{dummy_root}/app/models/foo.rb", + "class Foo < ApplicationRecord; end", + "Foo", + "Bar", + ) + + assert_empty(document_changes) + end + + test "does nothing when file name does not match class name" do + document_changes = collect_file_renames( + "#{dummy_root}/db/migrate/20210901000000_something_else.rb", + "class CreateFoos < ActiveRecord::Migration[7.0]; end", + "CreateFoos", + "CreateBars", + ) + + assert_empty(document_changes) + end + + private + + def collect_file_renames(file_path, source, old_name, new_name) + index = RubyIndexer::Index.new + uri = URI::Generic.from_path(path: file_path) + index.index_single(uri, source) + + document_changes = [] + Rename.new(index, old_name, new_name, document_changes) + document_changes + end + end + end +end