From 4b3febd72798799955d4cb7e6d91397456786427 Mon Sep 17 00:00:00 2001 From: krsinghshubham Date: Thu, 19 Feb 2026 01:35:19 +0530 Subject: [PATCH 1/2] Implement IRB 'fix' command to rerun with corrected spelling (issue #179) - Add fix command that reruns the previous command with Did you mean? correction - Only applies when exactly one suggestion exists with edit distance <= 2 - Supports NoMethodError, NameError, KeyError, LoadError, NoMatchingPatternKeyError - Auto-loads when IRB is used - Document in README --- README.md | 16 +++ lib/did_you_mean.rb | 5 + lib/did_you_mean/irb.rb | 165 +++++++++++++++++++++++++++++ lib/did_you_mean/irb/command.rb | 11 ++ lib/did_you_mean/irb/eval_patch.rb | 50 +++++++++ 5 files changed, 247 insertions(+) create mode 100644 lib/did_you_mean/irb.rb create mode 100644 lib/did_you_mean/irb/command.rb create mode 100644 lib/did_you_mean/irb/eval_patch.rb diff --git a/README.md b/README.md index 3049d01..135eafc 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,22 @@ hash => {fooo:} # Did you mean? :foo ``` +## IRB `fix` command + +When using IRB, after a typo causes a "Did you mean?" error, you can type `fix` to +automatically rerun the previous command with the correction applied. This only +works when there is exactly one suggestion with edit distance ≤ 2. + +```ruby +irb(main):001> 1.zeor? +# => NoMethodError (undefined method `zeor?' for 1:Integer) +# Did you mean? zero? + +irb(main):002> fix +# Rerunning with: 1.zero? +# => false +``` + ## Using the `DidYouMean::SpellChecker` If you need to programmatically find the closest matches to the user input, you could do so by re-using the `DidYouMean::SpellChecker` object. diff --git a/lib/did_you_mean.rb b/lib/did_you_mean.rb index 74cd176..c69d6a6 100644 --- a/lib/did_you_mean.rb +++ b/lib/did_you_mean.rb @@ -129,3 +129,8 @@ def self.formatter=(formatter) end end end + +# Auto-load IRB extension when IRB is used (enables `fix` command to rerun with corrections) +if defined?(::IRB) + require_relative "did_you_mean/irb" +end diff --git a/lib/did_you_mean/irb.rb b/lib/did_you_mean/irb.rb new file mode 100644 index 0000000..453a144 --- /dev/null +++ b/lib/did_you_mean/irb.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +# IRB integration for did_you_mean: allows rerunning the previous command with +# corrected spelling when a typo causes a Correctable error. +# +# After a NoMethodError, NameError, KeyError, or similar with "Did you mean?" +# suggestions, type `fix` to automatically rerun the command with the correction +# applied (only when there is exactly one suggestion with edit distance <= 2). +# +# See: https://github.com/ruby/did_you_mean/issues/179 + +module DidYouMean + module IRB + MAX_EDIT_DISTANCE = 2 + + # Stores the last failed code and exception for the fix command + module LastError + @last_code = nil + @last_exception = nil + @last_line_no = 1 + + class << self + attr_accessor :last_code, :last_exception, :last_line_no + + def store(code, exception, line_no) + self.last_code = code + self.last_exception = exception + self.last_line_no = line_no + end + + def clear + self.last_code = nil + self.last_exception = nil + self.last_line_no = 1 + end + end + end + + # Command that reruns the previous command with corrected spelling + class FixCommand + class << self + def category(category = nil) + @category = category if category + @category || "did_you_mean" + end + + def description(description = nil) + @description = description if description + @description || "Rerun the previous command with corrected spelling from Did you mean?" + end + + def execute(irb_context, _arg) + new(irb_context).execute + end + end + + def initialize(irb_context) + @irb_context = irb_context + end + + def execute + code = LastError.last_code + exception = LastError.last_exception + + if code.nil? || exception.nil? + puts "No previous error with Did you mean? suggestions. Try making a typo first, e.g. 1.zeor?" + return + end + + unless correctable?(exception) + puts "Last error is not correctable. The fix command only works with NoMethodError, NameError, KeyError, etc." + return + end + + wrong_str, correction = extract_correction(exception) + return unless correction + + corrected_code = apply_correction(code, wrong_str, correction) + return unless corrected_code + + puts "Rerunning with: #{corrected_code}" + eval_path = @irb_context.instance_variable_get(:@eval_path) || "(irb)" + result = @irb_context.workspace.evaluate(corrected_code, eval_path, LastError.last_line_no) + @irb_context.set_last_value(result) + @irb_context.irb.output_value if @irb_context.echo? + LastError.clear + end + + private + + def correctable?(exception) + exception.respond_to?(:corrections) && exception.is_a?(Exception) + end + + def extract_correction(exception) + corrections = exception.corrections + return [nil, nil] if corrections.nil? || corrections.empty? + + wrong_str = wrong_string_from(exception) + return [nil, nil] if wrong_str.nil? || wrong_str.to_s.empty? + + # Filter to corrections with edit distance <= MAX_EDIT_DISTANCE + filtered = corrections.select do |c| + correction_str = c.is_a?(Array) ? c.first.to_s : c.to_s + Levenshtein.distance(normalize(wrong_str), normalize(correction_str)) <= MAX_EDIT_DISTANCE + end + + # Only apply if exactly one match (per maintainer feedback) + return [nil, nil] unless filtered.size == 1 + + correction = filtered.first + correction_str = correction.is_a?(Array) ? correction.first : correction + [wrong_str.to_s, correction_str] + end + + def wrong_string_from(exception) + case exception + when NoMethodError + exception.name.to_s + when NameError + exception.name.to_s + when KeyError + exception.key.to_s + when defined?(NoMatchingPatternKeyError) && NoMatchingPatternKeyError + exception.key.to_s + when LoadError + # Extract path from message: "cannot load such file -- net-http" + exception.message[/cannot load such file -- (.+)/, 1] + else + nil + end + end + + def normalize(str) + str.to_s.downcase + end + + def apply_correction(code, wrong_str, correction_str) + correction_display = correction_str.to_s + + # Try different replacement patterns (method names, symbols, strings) + patterns = [ + [wrong_str, correction_display], # zeor? -> zero? + [":#{wrong_str}", ":#{correction_display}"], # :fooo -> :foo + ["\"#{wrong_str}\"", "\"#{correction_display}\""], # "fooo" -> "foo" + ["'#{wrong_str}'", "'#{correction_display}'"], # 'fooo' -> 'foo' + ] + + patterns.each do |wrong_pattern, correct_pattern| + escaped = Regexp.escape(wrong_pattern) + new_code = code.sub(/#{escaped}/, correct_pattern) + return new_code if new_code != code + end + + nil + end + end + end +end + +# Only load when IRB is available +if defined?(::IRB) + require "did_you_mean/irb/eval_patch" + require "did_you_mean/irb/command" +end diff --git a/lib/did_you_mean/irb/command.rb b/lib/did_you_mean/irb/command.rb new file mode 100644 index 0000000..b965e05 --- /dev/null +++ b/lib/did_you_mean/irb/command.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Registers the `fix` command with IRB + +require "irb/command" + +::IRB::Command._register_with_aliases( + :irb_fix, + DidYouMean::IRB::FixCommand, + [:fix, ::IRB::Command::NO_OVERRIDE] +) diff --git a/lib/did_you_mean/irb/eval_patch.rb b/lib/did_you_mean/irb/eval_patch.rb new file mode 100644 index 0000000..e020bea --- /dev/null +++ b/lib/did_you_mean/irb/eval_patch.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Patches IRB's eval_input to capture the last failed code and exception +# when a Did you mean? correctable error occurs. + +module DidYouMean + module IRB + module EvalPatch + def eval_input + configure_io + + each_top_level_statement do |statement, line_no| + signal_status(:IN_EVAL) do + begin + if @context.with_debugger && statement.should_be_handled_by_debugger? + return statement.code + end + + @context.evaluate(statement, line_no) + + if @context.echo? && !statement.suppresses_echo? + if statement.is_assignment? + if @context.echo_on_assignment? + output_value(@context.echo_on_assignment? == :truncate) + end + else + output_value + end + end + rescue SystemExit, SignalException + raise + rescue Interrupt, Exception => exc + # Store for fix command when it's a Correctable error + if exc.respond_to?(:corrections) && !exc.corrections.to_a.empty? + LastError.store(statement.code, exc, line_no) + else + LastError.clear + end + handle_exception(exc) + @context.workspace.local_variable_set(:_, exc) + end + end + end + end + end + end +end + +# Prepend our module to override eval_input +::IRB::Irb.prepend(DidYouMean::IRB::EvalPatch) From 37f5f6f8a5a6caebcf14a95638199e3c919cea25 Mon Sep 17 00:00:00 2001 From: krsinghshubham Date: Thu, 19 Feb 2026 02:32:36 +0530 Subject: [PATCH 2/2] Add discoverability hint and dym alias for fix command - Show 'Type `fix` to rerun with the correction.' when fix is available - Add dym alias for users who use fix as a variable - Document naming conflict and dym alternative in README Co-authored-by: Cursor --- README.md | 7 +++++++ lib/did_you_mean/irb.rb | 10 ++++++++++ lib/did_you_mean/irb/command.rb | 3 ++- lib/did_you_mean/irb/eval_patch.rb | 3 +++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 135eafc..4b5daf5 100644 --- a/README.md +++ b/README.md @@ -84,16 +84,23 @@ When using IRB, after a typo causes a "Did you mean?" error, you can type `fix` automatically rerun the previous command with the correction applied. This only works when there is exactly one suggestion with edit distance ≤ 2. +When a fix is available, IRB will show a hint so you know the command exists: + ```ruby irb(main):001> 1.zeor? # => NoMethodError (undefined method `zeor?' for 1:Integer) # Did you mean? zero? +# Type `fix` to rerun with the correction. irb(main):002> fix # Rerunning with: 1.zero? # => false ``` +**Note:** The `fix` command is an IRB command and takes precedence over any local +variable named `fix`. If you use `fix` as a variable, type `dym` instead (short for +"did you mean"), or access your variable via `binding.local_variable_get(:fix)`. + ## Using the `DidYouMean::SpellChecker` If you need to programmatically find the closest matches to the user input, you could do so by re-using the `DidYouMean::SpellChecker` object. diff --git a/lib/did_you_mean/irb.rb b/lib/did_you_mean/irb.rb index 453a144..16abe9c 100644 --- a/lib/did_you_mean/irb.rb +++ b/lib/did_you_mean/irb.rb @@ -58,6 +58,16 @@ def initialize(irb_context) @irb_context = irb_context end + def self.fixable? + return false if LastError.last_code.nil? || LastError.last_exception.nil? + cmd = allocate + cmd.instance_variable_set(:@irb_context, nil) + return false unless cmd.send(:correctable?, LastError.last_exception) + wrong_str, correction = cmd.send(:extract_correction, LastError.last_exception) + return false if correction.nil? + !cmd.send(:apply_correction, LastError.last_code, wrong_str, correction).nil? + end + def execute code = LastError.last_code exception = LastError.last_exception diff --git a/lib/did_you_mean/irb/command.rb b/lib/did_you_mean/irb/command.rb index b965e05..8e18c72 100644 --- a/lib/did_you_mean/irb/command.rb +++ b/lib/did_you_mean/irb/command.rb @@ -7,5 +7,6 @@ ::IRB::Command._register_with_aliases( :irb_fix, DidYouMean::IRB::FixCommand, - [:fix, ::IRB::Command::NO_OVERRIDE] + [:fix, ::IRB::Command::NO_OVERRIDE], + [:dym, ::IRB::Command::NO_OVERRIDE] ) diff --git a/lib/did_you_mean/irb/eval_patch.rb b/lib/did_you_mean/irb/eval_patch.rb index e020bea..5701cc4 100644 --- a/lib/did_you_mean/irb/eval_patch.rb +++ b/lib/did_you_mean/irb/eval_patch.rb @@ -37,6 +37,9 @@ def eval_input LastError.clear end handle_exception(exc) + if DidYouMean::IRB::FixCommand.fixable? + puts "\e[2mType `fix` to rerun with the correction.\e[0m" + end @context.workspace.local_variable_set(:_, exc) end end