defmodule Mix.Tasks.Ragex.Refactor do @moduledoc """ Interactive refactoring wizard with operation selection and preview. ## Usage # Launch interactive wizard mix ragex.refactor # Direct refactoring with parameters (skips wizard) mix ragex.refactor --operation rename_function --module MyModule --function old_name --new-name new_name Provides an interactive TUI for selecting and performing refactoring operations: - rename_function: Rename a function across call sites - rename_module: Rename a module and update references - change_signature: Modify function parameters - extract_function: Extract code into new function - inline_function: Inline function body into call sites Features: - Interactive operation selection - Fuzzy search for modules and functions - Parameter validation - Diff preview before applying - Conflict detection and warnings - Progress tracking for multi-file operations """ @shortdoc "Interactive refactoring wizard" use Mix.Task alias Ragex.CLI.{Colors, Output, Prompt} alias Ragex.Editor.Refactor alias Ragex.Graph.Store @operations [ {:rename_function, "Rename a function"}, {:rename_module, "Rename a module"}, {:change_signature, "Modify function signature"}, {:extract_function, "Extract code into new function"}, {:inline_function, "Inline function body"} ] @impl Mix.Task def run(args) do {:ok, _} = Application.ensure_all_started(:ragex) {opts, _, _} = OptionParser.parse(args, switches: [ operation: :string, module: :string, function: :string, arity: :integer, new_name: :string, help: :boolean ], aliases: [ o: :operation, m: :module, f: :function, a: :arity, n: :new_name, h: :help ] ) cond do opts[:help] -> show_help() opts[:operation] -> run_direct_refactoring(opts) true -> run_interactive_wizard() end end # Interactive wizard flow defp run_interactive_wizard do Output.section("Ragex Refactoring Wizard") # Check if codebase is analyzed unless codebase_ready?() do IO.puts(Colors.error("✗ Codebase not analyzed")) IO.puts(Colors.muted("Run analysis first: mix ragex.cache.refresh --path .")) IO.puts("") System.halt(1) end # Step 1: Select operation operation = select_operation() # Step 2: Gather parameters based on operation params = gather_parameters(operation) # Step 3: Preview changes unless preview_changes(operation, params) do IO.puts(Colors.muted("Refactoring cancelled.")) IO.puts("") System.halt(0) end # Step 4: Execute refactoring execute_refactoring(operation, params) end defp codebase_ready? do match?([_ | _], Store.list_modules()) end defp select_operation do IO.puts(Colors.bold("Select refactoring operation:")) IO.puts("") operation_labels = Enum.map(@operations, fn {op, desc} -> "#{Colors.info(Atom.to_string(op))} - #{desc}" end) selected = Prompt.select("Select refactoring operation:", operation_labels, default: 0) selected_index = Enum.find_index(operation_labels, &(&1 == selected)) || 0 {operation, _desc} = Enum.at(@operations, selected_index) IO.puts("") IO.puts(Colors.success("Selected: #{operation}")) IO.puts("") operation end defp gather_parameters(:rename_function) do IO.puts(Colors.bold("Rename Function Parameters")) IO.puts("") # Get module modules = Store.list_modules() IO.puts("Available modules: #{length(modules)}") module_str = Prompt.input("Module name", validate: &validate_module(&1, modules)) module = String.to_atom(module_str) # Get function functions = get_module_functions(module) IO.puts("Available functions: #{length(functions)}") function_str = Prompt.input("Function name", validate: &validate_non_empty/1) function = String.to_atom(function_str) # Get arity arity = Prompt.number("Function arity", min: 0, max: 20) # Verify function exists unless function_exists?(module, function, arity) do IO.puts( Colors.error("✗ Function #{module}.#{function}/#{arity} not found in knowledge graph") ) IO.puts("") System.halt(1) end # Get new name new_name_str = Prompt.input("New function name", validate: &validate_non_empty/1, default: Atom.to_string(function) ) new_name = String.to_atom(new_name_str) # Get scope IO.puts("") IO.puts("Refactoring scope:") scope_options = [ "module - Rename only within #{module}", "project - Rename across entire project" ] scope_selected = Prompt.select("Refactoring scope:", scope_options, default: 1) scope = if scope_selected == List.first(scope_options), do: :module, else: :project IO.puts("") %{ module: module, function: function, arity: arity, new_name: new_name, scope: scope } end defp gather_parameters(:rename_module) do IO.puts(Colors.bold("Rename Module Parameters")) IO.puts("") # Get current module modules = Store.list_modules() IO.puts("Available modules: #{length(modules)}") old_module_str = Prompt.input("Current module name", validate: &validate_module(&1, modules)) old_module = String.to_atom(old_module_str) # Get new name new_module_str = Prompt.input("New module name", validate: &validate_non_empty/1, default: old_module_str ) new_module = String.to_atom(new_module_str) IO.puts("") %{ old_module: old_module, new_module: new_module } end defp gather_parameters(:change_signature) do IO.puts(Colors.bold("Change Signature Parameters")) IO.puts("") # Get module and function modules = Store.list_modules() IO.puts("Available modules: #{length(modules)}") module_str = Prompt.input("Module name", validate: &validate_module(&1, modules)) module = String.to_atom(module_str) function_str = Prompt.input("Function name", validate: &validate_non_empty/1) function = String.to_atom(function_str) arity = Prompt.number("Current arity", min: 0, max: 20) unless function_exists?(module, function, arity) do IO.puts( Colors.error("✗ Function #{module}.#{function}/#{arity} not found in knowledge graph") ) IO.puts("") System.halt(1) end # Get signature changes IO.puts("") IO.puts(Colors.bold("Signature modifications:")) IO.puts("(Enter empty operation to finish)") IO.puts("") changes = collect_signature_changes() %{ module: module, function: function, arity: arity, changes: changes } end defp gather_parameters(:extract_function) do IO.puts(Colors.bold("Extract Function Parameters")) IO.puts("") modules = Store.list_modules() IO.puts("Available modules: #{length(modules)}") module_str = Prompt.input("Module name", validate: &validate_module(&1, modules)) module = String.to_atom(module_str) source_function_str = Prompt.input("Source function name", validate: &validate_non_empty/1) source_function = String.to_atom(source_function_str) source_arity = Prompt.number("Source function arity", min: 0, max: 20) unless function_exists?(module, source_function, source_arity) do IO.puts(Colors.error("✗ Function #{module}.#{source_function}/#{source_arity} not found")) IO.puts("") System.halt(1) end new_function_str = Prompt.input("New function name", validate: &validate_non_empty/1) new_function = String.to_atom(new_function_str) line_start = Prompt.number("Start line", min: 1) line_end = Prompt.number("End line", min: line_start) IO.puts("") %{ module: module, source_function: source_function, source_arity: source_arity, new_function: new_function, line_start: line_start, line_end: line_end } end defp gather_parameters(:inline_function) do IO.puts(Colors.bold("Inline Function Parameters")) IO.puts("") modules = Store.list_modules() IO.puts("Available modules: #{length(modules)}") module_str = Prompt.input("Module name", validate: &validate_module(&1, modules)) module = String.to_atom(module_str) function_str = Prompt.input("Function name", validate: &validate_non_empty/1) function = String.to_atom(function_str) arity = Prompt.number("Function arity", min: 0, max: 20) unless function_exists?(module, function, arity) do IO.puts( Colors.error("✗ Function #{module}.#{function}/#{arity} not found in knowledge graph") ) IO.puts("") System.halt(1) end IO.puts("") %{ module: module, function: function, arity: arity } end defp collect_signature_changes do change_types = ["add", "remove", "rename", "reorder"] collect_changes([], fn changes -> IO.puts("Change #{length(changes) + 1}:") type_index = Prompt.select(change_types ++ ["done"], default: 0) if type_index == length(change_types) do {:done, changes} else change_type = Enum.at(change_types, type_index) |> String.to_atom() change = collect_single_change(change_type) {:continue, changes ++ [change]} end end) end defp collect_changes(acc, collector) do case collector.(acc) do {:done, result} -> result {:continue, new_acc} -> collect_changes(new_acc, collector) end end defp collect_single_change(:add) do name = Prompt.input("Parameter name", validate: &validate_non_empty/1) position = Prompt.number("Position (0-based)", min: 0) default = Prompt.input("Default value (optional)", default: "nil") {:add, name, position, default} end defp collect_single_change(:remove) do name = Prompt.input("Parameter name to remove", validate: &validate_non_empty/1) {:remove, name} end defp collect_single_change(:rename) do old_name = Prompt.input("Current parameter name", validate: &validate_non_empty/1) new_name = Prompt.input("New parameter name", validate: &validate_non_empty/1) {:rename, old_name, new_name} end defp collect_single_change(:reorder) do positions_str = Prompt.input("New order (comma-separated indices)", validate: &validate_non_empty/1) positions = String.split(positions_str, ",") |> Enum.map(&String.to_integer(String.trim(&1))) {:reorder, positions} end defp preview_changes(operation, params) do Output.section("Preview Changes") IO.puts(Colors.info("Operation: #{operation}")) IO.puts(Colors.muted("Parameters:")) Enum.each(params, fn {key, value} -> IO.puts(Colors.muted(" #{key}: #{inspect(value)}")) end) IO.puts("") IO.puts(Colors.warning("Note: Actual diff preview requires implementation")) IO.puts("") Prompt.confirm("Apply refactoring?", default: :yes) end defp execute_refactoring(:rename_function, params) do Output.section("Executing Refactoring") result = Refactor.rename_function( params.module, params.function, params.new_name, params.arity, scope: params.scope, validate: true, format: true ) handle_result(result) end defp execute_refactoring(:rename_module, params) do Output.section("Executing Refactoring") result = Refactor.rename_module( params.old_module, params.new_module, validate: true, format: true ) handle_result(result) end defp execute_refactoring(:change_signature, params) do Output.section("Executing Refactoring") result = Refactor.change_signature( params.module, params.function, params.arity, params.changes, validate: true, format: true ) handle_result(result) end defp execute_refactoring(:extract_function, params) do Output.section("Executing Refactoring") result = Refactor.extract_function( params.module, params.source_function, params.source_arity, params.new_function, {params.line_start, params.line_end}, validate: true, format: true ) handle_result(result) end defp execute_refactoring(:inline_function, params) do Output.section("Executing Refactoring") result = Refactor.inline_function( params.module, params.function, params.arity, validate: true, format: true ) handle_result(result) end defp handle_result({:ok, result}) do IO.puts(Colors.success("✓ Refactoring completed successfully")) IO.puts("") Output.key_value( [ {"Files modified", Colors.highlight(to_string(result.files_edited))}, {"Lines changed", result.lines_added + result.lines_removed} ], indent: 2 ) IO.puts("") if result.warnings != [] do IO.puts(Colors.warning("Warnings:")) Enum.each(result.warnings, fn warning -> IO.puts(Colors.muted(" • #{warning}")) end) IO.puts("") end end defp handle_result({:error, result}) do IO.puts(Colors.error("✗ Refactoring failed")) IO.puts("") if result.errors != [] do IO.puts(Colors.error("Errors:")) Enum.each(result.errors, fn {file, error} -> IO.puts(Colors.error(" • #{file}: #{inspect(error)}")) end) IO.puts("") end IO.puts(Colors.muted("Changes rolled back.")) IO.puts("") System.halt(1) end # Direct refactoring (non-interactive) defp run_direct_refactoring(opts) do operation = String.to_atom(opts[:operation]) params = case operation do :rename_function -> %{ module: String.to_atom(opts[:module]), function: String.to_atom(opts[:function]), arity: opts[:arity], new_name: String.to_atom(opts[:new_name]), scope: :project } _ -> IO.puts(Colors.error("Direct refactoring only supports rename_function currently")) System.halt(1) end execute_refactoring(operation, params) end # Validation helpers defp validate_module(input, modules) do module = String.to_atom(input) if Enum.member?(modules, module) do :ok else {:error, "Module not found in knowledge graph"} end end defp validate_non_empty(""), do: {:error, "Cannot be empty"} defp validate_non_empty(_), do: :ok defp function_exists?(module, function, arity) do Store.get_function(module, function, arity) != nil end defp get_module_functions(module) do # Get all functions defined in module case Store.get_module(module) do nil -> [] _ -> Store.list_functions(module) end end defp show_help do IO.puts(""" #{Colors.bold("Ragex Refactoring Wizard")} #{Colors.info("Interactive mode:")} mix ragex.refactor #{Colors.info("Direct mode (rename_function only):")} mix ragex.refactor --operation rename_function \\ --module MyModule \\ --function old_name \\ --arity 2 \\ --new-name new_name #{Colors.info("Supported operations:")} • rename_function - Rename a function across call sites • rename_module - Rename a module and update references • change_signature - Modify function parameters • extract_function - Extract code into new function • inline_function - Inline function body into call sites #{Colors.muted("Note: Codebase must be analyzed first (mix ragex.cache.refresh)")} """) end end