Skip to content

Commit

Permalink
Merge branch 'omm/fuzzy'
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeInnes committed Jan 28, 2015
2 parents 97e9499 + 29b7f67 commit 8e1e310
Showing 1 changed file with 172 additions and 3 deletions.
175 changes: 172 additions & 3 deletions base/docs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -351,13 +351,182 @@ catdoc(md::MD...) = MD(md...)

# REPL help

function repl_search(s)
pre = "search:"
print(pre)
printmatches(s, completions(s), cols=Base.tty_size()[2]-length(pre))
println("\n")
end

function repl_corrections(s)
print("Couldn't find ")
Markdown.with_output_format(:cyan, STDOUT) do io
println(io, s)
end
print_correction(s)
end

macro repl (ex)
quote
# Backwards-compatible with the previous help system, for now
let doc = @doc $(esc(ex))
doc nothing ? doc : Base.Help.@help_ $(esc(ex))
# Fuzzy Searching
$(isexpr(ex, Symbol)) && repl_search($(string(ex)))
if $(isa(ex, Symbol)) && !isdefined($(current_module()), $(Expr(:quote, ex)))
repl_corrections($(string(ex)))
else
# Backwards-compatible with the previous help system, for now
let doc = @doc $(esc(ex))
doc nothing ? doc : Base.Help.@help_ $(esc(ex))
end
end
end
end

# Search & Rescue
# Utilities for correcting user mistakes and (eventually)
# doing full documentation searches from the repl.

# Fuzzy Search Algorithm

function matchinds(needle, haystack; acronym = false)
chars = collect(needle)
is = Int[]
lastc = '\0'
for (i, char) in enumerate(haystack)
isempty(chars) && break
while chars[1] == ' ' shift!(chars) end # skip spaces
if lowercase(char) == lowercase(chars[1]) && (!acronym || !isalpha(lastc))
push!(is, i)
shift!(chars)
end
lastc = char
end
return is
end

longer(x, y) = length(x) length(y) ? (x, true) : (y, false)

bestmatch(needle, haystack) =
longer(matchinds(needle, haystack, acronym = true),
matchinds(needle, haystack))

avgdistance(xs) =
isempty(xs) ? 0 :
(xs[end] - xs[1] - length(xs)+1)/length(xs)

function fuzzyscore(needle, haystack)
score = 0.
is, acro = bestmatch(needle, haystack)
score += (acro?2:1)length(is) # Matched characters
score -= 2(length(needle)-length(is)) # Missing characters
!acro && (score -= avgdistance(is)/10) # Contiguous
!isempty(is) && (score -= mean(is)/100) # Closer to beginning
end

function fuzzysort(search, candidates)
scores = map(cand -> (fuzzyscore(search, cand), -levenshtein(search, cand)), candidates)
candidates[sortperm(scores)] |> reverse
end

# Levenshtein Distance

function levenshtein(s1, s2)
a, b = collect(s1), collect(s2)
m = length(a)
n = length(b)
d = Array(Int, m+1, n+1)

d[1:m+1, 1] = 0:m
d[1, 1:n+1] = 0:n

for i = 1:m, j = 1:n
d[i+1,j+1] = min(d[i , j+1] + 1,
d[i+1, j ] + 1,
d[i , j ] + (a[i] != b[j]))
end

return d[m+1, n+1]
end

function levsort(search, candidates)
scores = map(cand -> (levenshtein(search, cand), -fuzzyscore(search, cand)), candidates)
candidates = candidates[sortperm(scores)]
i = 0
for i = 1:length(candidates)
levenshtein(search, candidates[i]) > 3 && break
end
return candidates[1:i]
end

# Result printing

function printmatch(io::IO, word, match)
is, _ = bestmatch(word, match)
Markdown.with_output_format(:fade, io) do io
for (i, char) = enumerate(match)
if i in is
Markdown.with_output_format(print, :bold, io, char)
else
print(io, char)
end
end
end
end

printmatch(args...) = printfuzzy(STDOUT, args...)

function printmatches(io::IO, word, matches; cols = Base.tty_size()[2])
total = 0
for match in matches
total + length(match) + 1 > cols && break
fuzzyscore(word, match) < 0 && break
print(io, " ")
printmatch(io, word, match)
total += length(match) + 1
end
end

printmatches(args...; cols = Base.tty_size()[2]) = printmatches(STDOUT, args..., cols = cols)

function print_joined_cols(io::IO, ss, delim = "", last = delim; cols = Base.tty_size()[2])
i = 0
total = 0
for i = 1:length(ss)
total += length(ss[i])
total + max(i-2,0)*length(delim) + (i>1?1:0)*length(last) > cols && (i-=1; break)
end
print_joined(io, ss[1:i], delim, last)
end

print_joined_cols(args...; cols = Base.tty_size()[2]) = print_joined_cols(STDOUT, args...; cols=cols)

function print_correction(word)
cors = levsort(word, accessible(current_module()))
pre = "Perhaps you meant "
print(pre)
print_joined_cols(cors, ", ", " or "; cols = Base.tty_size()[2]-length(pre))
println()
return
end

# Completion data

const builtins = ["abstract", "baremodule", "begin", "bitstype", "break",
"catch", "ccall", "const", "continue", "do", "else",
"elseif", "end", "export", "finally", "for", "function",
"global", "if", "immutable", "import", "importall", "let",
"local", "macro", "module", "quote", "return", "try", "type",
"typealias", "using", "while"]

moduleusings(mod) = ccall(:jl_module_usings, Any, (Any,), mod)

filtervalid(names) = filter(x->!ismatch(r"#", x), map(string, names))

accessible(mod::Module) =
[names(mod, true, true),
map(names, moduleusings(mod))...,
builtins] |> unique |> filtervalid

completions(name) = fuzzysort(name, accessible(current_module()))
completions(name::Symbol) = completions(string(name))

end

0 comments on commit 8e1e310

Please sign in to comment.