Skip to content
This repository has been archived by the owner on Apr 14, 2021. It is now read-only.

Commit

Permalink
Suggest alternatives when typoing gem names
Browse files Browse the repository at this point in the history
eg
$ bundle open thinking_sphinx
Could not find gem 'thinking_sphinx'.
Did you mean thinking-sphinx?
  • Loading branch information
Jonathan del Strother authored and Jonathan del Strother committed Aug 20, 2012
1 parent 4d163e8 commit a567307
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 2 deletions.
16 changes: 14 additions & 2 deletions 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
Expand Down Expand Up @@ -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
Expand All @@ -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
63 changes: 63 additions & 0 deletions 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
5 changes: 5 additions & 0 deletions spec/other/open_spec.rb
Expand Up @@ -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
4 changes: 4 additions & 0 deletions spec/update/gems_spec.rb
Expand Up @@ -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
Expand Down

0 comments on commit a567307

Please sign in to comment.