Skip to content
This repository

Suggest alternatives when typoing gem names #2069

Merged
merged 1 commit into from over 1 year ago

3 participants

Jonathan del Strother Don't Add Me To Your Organization a.k.a The Travis Bot André Arko
Jonathan del Strother

eg
$ bundle open thinking_sphinx
Could not find gem 'thinking_sphinx'.
Did you mean thinking-sphinx?

This is basically a repeat of #2050, but now supports both bundle-open and bundle-update typos, and is somewhat refactored.
I'm not loving the implementation, but it works fine... would be open to any suggestions.

Jonathan del Strother Suggest alternatives when typoing gem names
eg
$ bundle open thinking_sphinx
Could not find gem 'thinking_sphinx'.
Did you mean thinking-sphinx?
a567307
Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged a567307 into 4d163e8).

André Arko
Owner

Excellent. Thanks for the patch, and the tests, they are both much appreciated. :)

André Arko indirect merged commit 472a53b into from August 20, 2012
André Arko indirect closed this August 20, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 1 author.

Aug 21, 2012
Jonathan del Strother Suggest alternatives when typoing gem names
eg
$ bundle open thinking_sphinx
Could not find gem 'thinking_sphinx'.
Did you mean thinking-sphinx?
a567307
This page is out of date. Refresh to see the latest.
16  lib/bundler/cli.rb
... ...
@@ -1,6 +1,7 @@
1 1
 require 'bundler/vendored_thor'
2 2
 require 'rubygems/user_interaction'
3 3
 require 'rubygems/config_file'
  4
+require 'bundler/similarity_detector'
4 5
 
5 6
 module Bundler
6 7
   class CLI < Thor
@@ -681,7 +682,7 @@ def have_groff?
681 682
 
682 683
     def locate_gem(name)
683 684
       spec = Bundler.load.specs.find{|s| s.name == name }
684  
-      raise GemNotFound, "Could not find gem '#{name}' in the current bundle." unless spec
  685
+      raise GemNotFound, not_found_message(name, Bundler.load.specs) unless spec
685 686
       if spec.name == 'bundler'
686 687
         return File.expand_path('../../../', __FILE__)
687 688
       end
@@ -690,9 +691,20 @@ def locate_gem(name)
690 691
 
691 692
     def gem_dependency_with_name(name)
692 693
       dep = Bundler.load.dependencies.find{|d| d.name == name }
693  
-      raise GemNotFound, "Could not find gem '#{name}'." unless dep
  694
+      raise GemNotFound, not_found_message(name, Bundler.load.dependencies) unless dep
694 695
       dep
695 696
     end
696 697
 
  698
+    def not_found_message(missing_gem_name, alternatives)
  699
+      message = "Could not find gem '#{missing_gem_name}'."
  700
+
  701
+      # This is called as the result of a GemNotFound, let's see if
  702
+      # there's any similarly named ones we can propose instead
  703
+      alternate_names = alternatives.map{|a| a.name}
  704
+      suggestions = SimilarityDetector.new(alternate_names).similar_word_list(missing_gem_name)
  705
+      message += "\nDid you mean #{suggestions}?" if suggestions
  706
+      message
  707
+    end
  708
+
697 709
   end
698 710
 end
63  lib/bundler/similarity_detector.rb
... ...
@@ -0,0 +1,63 @@
  1
+module Bundler
  2
+  class SimilarityDetector
  3
+    SimilarityScore = Struct.new(:string, :distance)
  4
+
  5
+    # initialize with an array of words to be matched against
  6
+    def initialize(corpus)
  7
+      @corpus = corpus
  8
+    end
  9
+
  10
+    # return an array of words similar to 'word' from the corpus
  11
+    def similar_words(word, limit=3)
  12
+      words_by_similarity = @corpus.map{|w| SimilarityScore.new(w, levenshtein_distance(word, w))}
  13
+      words_by_similarity.select{|s| s.distance<=limit}.sort_by(&:distance).map(&:string)
  14
+    end
  15
+
  16
+    # return the result of 'similar_words', concatenated into a list
  17
+    # (eg "a, b, or c")
  18
+    def similar_word_list(word, limit=3)
  19
+      words = similar_words(word,limit)
  20
+      if words.length==1
  21
+        words[0]
  22
+      elsif words.length>1
  23
+        [words[0..-2].join(', '), words[-1]].join(' or ')
  24
+      end
  25
+    end
  26
+
  27
+
  28
+  protected
  29
+    # http://www.informit.com/articles/article.aspx?p=683059&seqNum=36
  30
+    def levenshtein_distance(this, that, ins=2, del=2, sub=1)
  31
+      # ins, del, sub are weighted costs
  32
+      return nil if this.nil?
  33
+      return nil if that.nil?
  34
+      dm = []        # distance matrix
  35
+
  36
+      # Initialize first row values
  37
+      dm[0] = (0..this.length).collect { |i| i * ins }
  38
+      fill = [0] * (this.length - 1)
  39
+
  40
+      # Initialize first column values
  41
+      for i in 1..that.length
  42
+        dm[i] = [i * del, fill.flatten]
  43
+      end
  44
+
  45
+      # populate matrix
  46
+      for i in 1..that.length
  47
+        for j in 1..this.length
  48
+          # critical comparison
  49
+          dm[i][j] = [
  50
+               dm[i-1][j-1] +
  51
+                 (this[j-1] == that[i-1] ? 0 : sub),
  52
+                   dm[i][j-1] + ins,
  53
+               dm[i-1][j] + del
  54
+         ].min
  55
+        end
  56
+      end
  57
+
  58
+      # The last value in matrix is the Levenshtein distance between the strings
  59
+      dm[that.length][this.length]
  60
+    end
  61
+
  62
+  end
  63
+end
5  spec/other/open_spec.rb
@@ -32,4 +32,9 @@
32 32
     bundle "open missing", :env => {"EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => ""}
33 33
     out.should match(/could not find gem 'missing'/i)
34 34
   end
  35
+
  36
+  it "suggests alternatives for similar-sounding gems" do
  37
+    bundle "open Rails", :env => {"EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => ""}
  38
+    out.should match(/did you mean rails\?/i)
  39
+  end
35 40
 end
4  spec/update/gems_spec.rb
@@ -61,6 +61,10 @@
61 61
       bundle "update halting-problem-solver", :expect_err=>true
62 62
       out.should include "Could not find gem 'halting-problem-solver'"
63 63
     end
  64
+    it "should suggest alternatives" do
  65
+      bundle "update active-support", :expect_err=>true
  66
+      out.should include "Did you mean activesupport?"
  67
+    end
64 68
   end
65 69
 
66 70
   describe "with --local option" do
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.