Browse files

Suggest alternatives when typoing gem names

eg
$ bundle open thinking_sphinx
Could not find gem 'thinking_sphinx'.
Did you mean thinking-sphinx?
  • Loading branch information...
1 parent 4d163e8 commit a567307a7fb05d23f1c1d47b2ce6c7abf0c38cb5 @jdelStrother jdelStrother committed Aug 13, 2012
Showing with 86 additions and 2 deletions.
  1. +14 −2 lib/bundler/cli.rb
  2. +63 −0 lib/bundler/similarity_detector.rb
  3. +5 −0 spec/other/open_spec.rb
  4. +4 −0 spec/update/gems_spec.rb
View
16 lib/bundler/cli.rb
@@ -1,6 +1,7 @@
require 'bundler/vendored_thor'
require 'rubygems/user_interaction'
require 'rubygems/config_file'
+require 'bundler/similarity_detector'
module Bundler
class CLI < Thor
@@ -681,7 +682,7 @@ def have_groff?
def locate_gem(name)
spec = Bundler.load.specs.find{|s| s.name == name }
- raise GemNotFound, "Could not find gem '#{name}' in the current bundle." unless spec
+ raise GemNotFound, not_found_message(name, Bundler.load.specs) unless spec
if spec.name == 'bundler'
return File.expand_path('../../../', __FILE__)
end
@@ -690,9 +691,20 @@ def locate_gem(name)
def gem_dependency_with_name(name)
dep = Bundler.load.dependencies.find{|d| d.name == name }
- raise GemNotFound, "Could not find gem '#{name}'." unless dep
+ raise GemNotFound, not_found_message(name, Bundler.load.dependencies) unless dep
dep
end
+ def not_found_message(missing_gem_name, alternatives)
+ message = "Could not find gem '#{missing_gem_name}'."
+
+ # This is called as the result of a GemNotFound, let's see if
+ # there's any similarly named ones we can propose instead
+ alternate_names = alternatives.map{|a| a.name}
+ suggestions = SimilarityDetector.new(alternate_names).similar_word_list(missing_gem_name)
+ message += "\nDid you mean #{suggestions}?" if suggestions
+ message
+ end
+
end
end
View
63 lib/bundler/similarity_detector.rb
@@ -0,0 +1,63 @@
+module Bundler
+ class SimilarityDetector
+ SimilarityScore = Struct.new(:string, :distance)
+
+ # initialize with an array of words to be matched against
+ def initialize(corpus)
+ @corpus = corpus
+ end
+
+ # return an array of words similar to 'word' from the corpus
+ def similar_words(word, limit=3)
+ words_by_similarity = @corpus.map{|w| SimilarityScore.new(w, levenshtein_distance(word, w))}
+ words_by_similarity.select{|s| s.distance<=limit}.sort_by(&:distance).map(&:string)
+ end
+
+ # return the result of 'similar_words', concatenated into a list
+ # (eg "a, b, or c")
+ def similar_word_list(word, limit=3)
+ words = similar_words(word,limit)
+ if words.length==1
+ words[0]
+ elsif words.length>1
+ [words[0..-2].join(', '), words[-1]].join(' or ')
+ end
+ end
+
+
+ protected
+ # http://www.informit.com/articles/article.aspx?p=683059&seqNum=36
+ def levenshtein_distance(this, that, ins=2, del=2, sub=1)
+ # ins, del, sub are weighted costs
+ return nil if this.nil?
+ return nil if that.nil?
+ dm = [] # distance matrix
+
+ # Initialize first row values
+ dm[0] = (0..this.length).collect { |i| i * ins }
+ fill = [0] * (this.length - 1)
+
+ # Initialize first column values
+ for i in 1..that.length
+ dm[i] = [i * del, fill.flatten]
+ end
+
+ # populate matrix
+ for i in 1..that.length
+ for j in 1..this.length
+ # critical comparison
+ dm[i][j] = [
+ dm[i-1][j-1] +
+ (this[j-1] == that[i-1] ? 0 : sub),
+ dm[i][j-1] + ins,
+ dm[i-1][j] + del
+ ].min
+ end
+ end
+
+ # The last value in matrix is the Levenshtein distance between the strings
+ dm[that.length][this.length]
+ end
+
+ end
+end
View
5 spec/other/open_spec.rb
@@ -32,4 +32,9 @@
bundle "open missing", :env => {"EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => ""}
out.should match(/could not find gem 'missing'/i)
end
+
+ it "suggests alternatives for similar-sounding gems" do
+ bundle "open Rails", :env => {"EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => ""}
+ out.should match(/did you mean rails\?/i)
+ end
end
View
4 spec/update/gems_spec.rb
@@ -61,6 +61,10 @@
bundle "update halting-problem-solver", :expect_err=>true
out.should include "Could not find gem 'halting-problem-solver'"
end
+ it "should suggest alternatives" do
+ bundle "update active-support", :expect_err=>true
+ out.should include "Did you mean activesupport?"
+ end
end
describe "with --local option" do

0 comments on commit a567307

Please sign in to comment.