mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-11-29 00:06:29 +00:00
Extended mode
- Implement prefix caching of extended mode - Improved ranking algorithm for extended mode - Fix nfc conversion bug
This commit is contained in:
parent
545e8bfcee
commit
43acf5c8a4
13
README.md
13
README.md
@ -114,6 +114,19 @@ The following readline key bindings should also work as expected.
|
|||||||
If you enable multi-select mode with `-m` option, you can select multiple items
|
If you enable multi-select mode with `-m` option, you can select multiple items
|
||||||
with TAB or Shift-TAB key.
|
with TAB or Shift-TAB key.
|
||||||
|
|
||||||
|
### Extended mode
|
||||||
|
|
||||||
|
With `-x` or `--extended` option, fzf will start in "extended mode".
|
||||||
|
|
||||||
|
In extended mode, you can specify multiple patterns delimited by spaces, such as: `^music .mp3$ sbtrkt !rmx`
|
||||||
|
|
||||||
|
| Token | Description | Match type |
|
||||||
|
| -------- | ----------------------------- | -------------------- |
|
||||||
|
| `^music` | Items that start with `music` | prefix-exact-match |
|
||||||
|
| `.mp3$` | Items that end with `.mp3` | suffix-exact-match |
|
||||||
|
| `sbtrkt` | Items that match `sbtrkt` | fuzzy-match |
|
||||||
|
| `!rmx` | Items that do not match `rmx` | invert-fuzzy-match |
|
||||||
|
|
||||||
Usage as Vim plugin
|
Usage as Vim plugin
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
7
Rakefile
7
Rakefile
@ -1 +1,8 @@
|
|||||||
require "bundler/gem_tasks"
|
require "bundler/gem_tasks"
|
||||||
|
require 'rake/testtask'
|
||||||
|
|
||||||
|
Rake::TestTask.new(:test) do |test|
|
||||||
|
test.pattern = 'test/**/test_*.rb'
|
||||||
|
test.verbose = true
|
||||||
|
end
|
||||||
|
|
||||||
|
79
fzf
79
fzf
@ -68,7 +68,7 @@ class FZF
|
|||||||
def initialize argv, source = $stdin
|
def initialize argv, source = $stdin
|
||||||
usage 0 unless (%w[--help -h] & argv).empty?
|
usage 0 unless (%w[--help -h] & argv).empty?
|
||||||
@rxflag = argv.delete('+i') ? 0 : Regexp::IGNORECASE
|
@rxflag = argv.delete('+i') ? 0 : Regexp::IGNORECASE
|
||||||
@sort = %w[+s --no-sort].map { |e| argv.delete e }.compact.empty? ?
|
@sort = %w[+s --no-sort].map { |e| argv.delete e }.compact.empty? ?
|
||||||
ENV.fetch('FZF_DEFAULT_SORT', 500).to_i : nil
|
ENV.fetch('FZF_DEFAULT_SORT', 500).to_i : nil
|
||||||
@color = %w[+c --no-color].map { |e| argv.delete e }.compact.empty?
|
@color = %w[+c --no-color].map { |e| argv.delete e }.compact.empty?
|
||||||
@multi = !%w[-m --multi].map { |e| argv.delete e }.compact.empty?
|
@multi = !%w[-m --multi].map { |e| argv.delete e }.compact.empty?
|
||||||
@ -149,6 +149,12 @@ class FZF
|
|||||||
ret
|
ret
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.to_nfc arr
|
||||||
|
[NFC_BEGIN + arr[0] * JJCOUNT +
|
||||||
|
(arr[1] || 0) * JONGSUNGS +
|
||||||
|
(arr[2] || 0)].pack('U*')
|
||||||
|
end
|
||||||
|
|
||||||
def self.nfc str, offsets = []
|
def self.nfc str, offsets = []
|
||||||
ret = ''
|
ret = ''
|
||||||
omap = []
|
omap = []
|
||||||
@ -165,9 +171,7 @@ class FZF
|
|||||||
next
|
next
|
||||||
else
|
else
|
||||||
omap[-1] = omap[-1] + 1
|
omap[-1] = omap[-1] + 1
|
||||||
ret << [NFC_BEGIN + pend[0] * JJCOUNT +
|
ret << to_nfc(pend)
|
||||||
(pend[1] || 0) * JONGSUNGS +
|
|
||||||
(pend[2] || 0)].pack('U*')
|
|
||||||
pend.clear
|
pend.clear
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -177,6 +181,7 @@ class FZF
|
|||||||
ret << c
|
ret << c
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
ret << to_nfc(pend) unless pend.empty?
|
||||||
return [ret,
|
return [ret,
|
||||||
offsets.map { |pair|
|
offsets.map { |pair|
|
||||||
b, e = pair
|
b, e = pair
|
||||||
@ -324,8 +329,14 @@ class FZF
|
|||||||
def sort_by_rank list
|
def sort_by_rank list
|
||||||
list.sort_by { |tuple|
|
list.sort_by { |tuple|
|
||||||
line, offsets = tuple
|
line, offsets = tuple
|
||||||
matchlen = (offsets.map { |pair| pair.last }.max || 0) -
|
matchlen = 0
|
||||||
(offsets.map { |pair| pair.first }.min || 0)
|
pe = nil
|
||||||
|
offsets.sort.each do |pair|
|
||||||
|
b, e = pair
|
||||||
|
b = pe if pe && pe > b
|
||||||
|
pe = e
|
||||||
|
matchlen += e - b
|
||||||
|
end
|
||||||
[matchlen, line.length, line]
|
[matchlen, line.length, line]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@ -453,7 +464,7 @@ class FZF
|
|||||||
|
|
||||||
def start_search
|
def start_search
|
||||||
main = Thread.current
|
main = Thread.current
|
||||||
matcher = (@xmode ? XFuzzyMatcher : FuzzyMatcher).new @rxflag
|
matcher = (@xmode ? ExtendedFuzzyMatcher : FuzzyMatcher).new @rxflag
|
||||||
searcher = Thread.new {
|
searcher = Thread.new {
|
||||||
lists = []
|
lists = []
|
||||||
events = {}
|
events = {}
|
||||||
@ -654,15 +665,14 @@ class FZF
|
|||||||
end
|
end
|
||||||
@stdout.puts got
|
@stdout.puts got
|
||||||
end
|
end
|
||||||
exit 0
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class FuzzyMatcher < Matcher
|
class FuzzyMatcher < Matcher
|
||||||
attr_reader :cache, :rxflag
|
attr_reader :caches, :rxflag
|
||||||
|
|
||||||
def initialize rxflag
|
def initialize rxflag
|
||||||
@cache = Hash.new { |h, k| h[k] = {} }
|
@caches = Hash.new { |h, k| h[k] = {} }
|
||||||
@regexp = {}
|
@regexp = {}
|
||||||
@rxflag = rxflag
|
@rxflag = rxflag
|
||||||
end
|
end
|
||||||
@ -680,7 +690,7 @@ class FZF
|
|||||||
def match list, q, prefix, suffix
|
def match list, q, prefix, suffix
|
||||||
regexp = fuzzy_regex q
|
regexp = fuzzy_regex q
|
||||||
|
|
||||||
cache = @cache[list.object_id]
|
cache = @caches[list.object_id]
|
||||||
prefix_cache = nil
|
prefix_cache = nil
|
||||||
(prefix.length - 1).downto(1) do |len|
|
(prefix.length - 1).downto(1) do |len|
|
||||||
break if prefix_cache = cache[prefix[0, len]]
|
break if prefix_cache = cache[prefix[0, len]]
|
||||||
@ -702,28 +712,49 @@ class FZF
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class XFuzzyMatcher < FuzzyMatcher
|
class ExtendedFuzzyMatcher < FuzzyMatcher
|
||||||
|
def initialize rxflag
|
||||||
|
super
|
||||||
|
require 'set'
|
||||||
|
@regexps = {}
|
||||||
|
end
|
||||||
|
|
||||||
def match list, q, prefix, suffix
|
def match list, q, prefix, suffix
|
||||||
regexps = q.strip.split(/\s+/).map { |w|
|
q = q.strip
|
||||||
|
|
||||||
|
regexps = @regexps[q] ||= q.split(/\s+/).map { |w|
|
||||||
invert =
|
invert =
|
||||||
if w =~ /^!/
|
if w =~ /^!/
|
||||||
w = w[1..-1]
|
w = w[1..-1]
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
[ case w
|
[ @regexp[w] ||=
|
||||||
when ''
|
case w
|
||||||
nil
|
when ''
|
||||||
when /^\^/
|
nil
|
||||||
w.length > 1 ? Regexp.new('^' << w[1..-1], rxflag) : nil
|
when /^\^/
|
||||||
when /\$$/
|
w.length > 1 ?
|
||||||
w.length > 1 ? Regexp.new(w[0..-2] << '$', rxflag) : nil
|
Regexp.new('^' << Regexp.escape(w[1..-1]), rxflag) : nil
|
||||||
else
|
when /\$$/
|
||||||
fuzzy_regex w
|
w.length > 1 ?
|
||||||
end, invert ]
|
Regexp.new(Regexp.escape(w[0..-2]) << '$', rxflag) : nil
|
||||||
|
else
|
||||||
|
fuzzy_regex w
|
||||||
|
end, invert ]
|
||||||
}.select { |pair| pair.first }
|
}.select { |pair| pair.first }
|
||||||
|
|
||||||
list.map { |line|
|
# Look for prefix cache
|
||||||
|
cache = @caches[list.object_id]
|
||||||
|
prefix = prefix.strip.sub(/\$\S+$/, '').sub(/!\S+$/, '')
|
||||||
|
prefix_cache = nil
|
||||||
|
(prefix.length - 1).downto(1) do |len|
|
||||||
|
break if prefix_cache = cache[Set[@regexps[prefix[0, len]]]]
|
||||||
|
end
|
||||||
|
|
||||||
|
cache[Set[regexps]] ||= (prefix_cache ?
|
||||||
|
prefix_cache.map { |e| e.first } :
|
||||||
|
list).map { |line|
|
||||||
offsets = []
|
offsets = []
|
||||||
regexps.all? { |pair|
|
regexps.all? { |pair|
|
||||||
regexp, invert = pair
|
regexp, invert = pair
|
||||||
|
170
test/test_fzf.rb
170
test/test_fzf.rb
@ -56,11 +56,15 @@ class TestFZF < MiniTest::Unit::TestCase
|
|||||||
|
|
||||||
def test_trim
|
def test_trim
|
||||||
fzf = FZF.new []
|
fzf = FZF.new []
|
||||||
assert_equal ['사.', 6], fzf.trim('가나다라마바사.', 4, true)
|
assert_equal ['사.', 6], fzf.trim('가나다라마바사.', 4, true)
|
||||||
assert_equal ['바사.', 5], fzf.trim('가나다라마바사.', 5, true)
|
assert_equal ['바사.', 5], fzf.trim('가나다라마바사.', 5, true)
|
||||||
assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 4, false)
|
assert_equal ['바사.', 5], fzf.trim('가나다라마바사.', 6, true)
|
||||||
assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 5, false)
|
assert_equal ['마바사.', 4], fzf.trim('가나다라마바사.', 7, true)
|
||||||
assert_equal ['가나a', 6], fzf.trim('가나ab라마바사.', 5, false)
|
assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 4, false)
|
||||||
|
assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 5, false)
|
||||||
|
assert_equal ['가나a', 6], fzf.trim('가나ab라마바사.', 5, false)
|
||||||
|
assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 6, false)
|
||||||
|
assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 7, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_format
|
def test_format
|
||||||
@ -107,7 +111,7 @@ class TestFZF < MiniTest::Unit::TestCase
|
|||||||
juicily
|
juicily
|
||||||
juiciness
|
juiciness
|
||||||
juicy]
|
juicy]
|
||||||
assert matcher.cache.empty?
|
assert matcher.caches.empty?
|
||||||
assert_equal(
|
assert_equal(
|
||||||
[["juice", [[0, 1]]],
|
[["juice", [[0, 1]]],
|
||||||
["juiceful", [[0, 1]]],
|
["juiceful", [[0, 1]]],
|
||||||
@ -115,10 +119,10 @@ class TestFZF < MiniTest::Unit::TestCase
|
|||||||
["juicily", [[0, 1]]],
|
["juicily", [[0, 1]]],
|
||||||
["juiciness", [[0, 1]]],
|
["juiciness", [[0, 1]]],
|
||||||
["juicy", [[0, 1]]]], matcher.match(list, 'j', '', '').sort)
|
["juicy", [[0, 1]]]], matcher.match(list, 'j', '', '').sort)
|
||||||
assert !matcher.cache.empty?
|
assert !matcher.caches.empty?
|
||||||
assert_equal [list.object_id], matcher.cache.keys
|
assert_equal [list.object_id], matcher.caches.keys
|
||||||
assert_equal 1, matcher.cache[list.object_id].length
|
assert_equal 1, matcher.caches[list.object_id].length
|
||||||
assert_equal 6, matcher.cache[list.object_id]['j'].length
|
assert_equal 6, matcher.caches[list.object_id]['j'].length
|
||||||
|
|
||||||
assert_equal(
|
assert_equal(
|
||||||
[["juicily", [[0, 5]]],
|
[["juicily", [[0, 5]]],
|
||||||
@ -128,14 +132,96 @@ class TestFZF < MiniTest::Unit::TestCase
|
|||||||
[["juicily", [[2, 5]]],
|
[["juicily", [[2, 5]]],
|
||||||
["juiciness", [[2, 5]]]], matcher.match(list, 'ii', '', '').sort)
|
["juiciness", [[2, 5]]]], matcher.match(list, 'ii', '', '').sort)
|
||||||
|
|
||||||
assert_equal 3, matcher.cache[list.object_id].length
|
assert_equal 3, matcher.caches[list.object_id].length
|
||||||
assert_equal 2, matcher.cache[list.object_id]['ii'].length
|
assert_equal 2, matcher.caches[list.object_id]['ii'].length
|
||||||
|
|
||||||
# TODO : partial_cache
|
# TODO : partial_cache
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_fuzzy_matcher_case_sensitive
|
||||||
|
assert_equal [['Fruit', [[0, 5]]]],
|
||||||
|
FZF::FuzzyMatcher.new(0).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
|
||||||
|
|
||||||
|
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
|
||||||
|
FZF::FuzzyMatcher.new(Regexp::IGNORECASE).
|
||||||
|
match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_extended_fuzzy_matcher
|
||||||
|
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
|
||||||
|
list = %w[
|
||||||
|
juice
|
||||||
|
juiceful
|
||||||
|
juiceless
|
||||||
|
juicily
|
||||||
|
juiciness
|
||||||
|
juicy
|
||||||
|
_juice]
|
||||||
|
match = proc { |q, prefix|
|
||||||
|
matcher.match(list, q, prefix, '').sort.map { |p| [p.first, p.last.sort] }
|
||||||
|
}
|
||||||
|
|
||||||
|
assert matcher.caches.empty?
|
||||||
|
3.times do
|
||||||
|
['y j', 'j y'].each do |pat|
|
||||||
|
(0..pat.length - 1).each do |prefix_length|
|
||||||
|
prefix = pat[0, prefix_length]
|
||||||
|
assert_equal(
|
||||||
|
[["juicily", [[0, 1], [6, 7]]],
|
||||||
|
["juicy", [[0, 1], [4, 5]]]],
|
||||||
|
match.call(pat, prefix))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# $
|
||||||
|
assert_equal [["juiceful", [[7, 8]]]], match.call('l$', '')
|
||||||
|
assert_equal [["juiceful", [[7, 8]]],
|
||||||
|
["juiceless", [[5, 6]]],
|
||||||
|
["juicily", [[5, 6]]]], match.call('l', '')
|
||||||
|
|
||||||
|
# ^
|
||||||
|
assert_equal list.length, match.call('j', '').length
|
||||||
|
assert_equal list.length - 1, match.call('^j', '').length
|
||||||
|
|
||||||
|
# !
|
||||||
|
assert_equal 0, match.call('!j', '').length
|
||||||
|
|
||||||
|
# ! + ^
|
||||||
|
assert_equal [["_juice", []]], match.call('!^j', '')
|
||||||
|
|
||||||
|
# ! + $
|
||||||
|
assert_equal list.length - 1, match.call('!l$', '').length
|
||||||
|
|
||||||
|
# ! + f
|
||||||
|
assert_equal [["juicy", [[4, 5]]]], match.call('y !l', '')
|
||||||
|
end
|
||||||
|
assert !matcher.caches.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_xfuzzy_matcher_prefix_cache
|
||||||
|
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
|
||||||
|
list = %w[
|
||||||
|
a.java
|
||||||
|
b.java
|
||||||
|
java.jive
|
||||||
|
c.java$
|
||||||
|
d.java
|
||||||
|
]
|
||||||
|
2.times do
|
||||||
|
assert_equal 5, matcher.match(list, 'java', 'java', '').length
|
||||||
|
assert_equal 3, matcher.match(list, 'java$', 'java$', '').length
|
||||||
|
assert_equal 1, matcher.match(list, 'java$$', 'java$$', '').length
|
||||||
|
|
||||||
|
assert_equal 0, matcher.match(list, '!java', '!java', '').length
|
||||||
|
assert_equal 4, matcher.match(list, '!^jav', '!^jav', '').length
|
||||||
|
assert_equal 4, matcher.match(list, '!^java', '!^java', '').length
|
||||||
|
assert_equal 2, matcher.match(list, '!^java !b !c', '!^java', '').length
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_sort_by_rank
|
def test_sort_by_rank
|
||||||
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
|
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
|
||||||
|
xmatcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
|
||||||
list = %w[
|
list = %w[
|
||||||
0____1
|
0____1
|
||||||
0_____1
|
0_____1
|
||||||
@ -146,7 +232,61 @@ class TestFZF < MiniTest::Unit::TestCase
|
|||||||
0______1
|
0______1
|
||||||
___01___
|
___01___
|
||||||
]
|
]
|
||||||
assert_equal %w[01 01_ _01_ ___01___ ____0_1 0____1 0_____1 0______1],
|
assert_equal(
|
||||||
FZF.new([]).sort_by_rank(matcher.match(list, '01', '', '')).map(&:first)
|
[["01", [[0, 2]]],
|
||||||
|
["01_", [[0, 2]]],
|
||||||
|
["_01_", [[1, 3]]],
|
||||||
|
["___01___", [[3, 5]]],
|
||||||
|
["____0_1", [[4, 7]]],
|
||||||
|
["0____1", [[0, 6]]],
|
||||||
|
["0_____1", [[0, 7]]],
|
||||||
|
["0______1", [[0, 8]]]],
|
||||||
|
FZF.new([]).sort_by_rank(matcher.match(list, '01', '', '')))
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
[["01", [[0, 1], [1, 2]]],
|
||||||
|
["01_", [[0, 1], [1, 2]]],
|
||||||
|
["_01_", [[1, 2], [2, 3]]],
|
||||||
|
["0____1", [[0, 1], [5, 6]]],
|
||||||
|
["0_____1", [[0, 1], [6, 7]]],
|
||||||
|
["____0_1", [[4, 5], [6, 7]]],
|
||||||
|
["0______1", [[0, 1], [7, 8]]],
|
||||||
|
["___01___", [[3, 4], [4, 5]]]],
|
||||||
|
FZF.new([]).sort_by_rank(xmatcher.match(list, '0 1', '', '')))
|
||||||
|
|
||||||
|
assert_equal(
|
||||||
|
[["_01_", [[1, 3], [0, 4]]],
|
||||||
|
["0____1", [[0, 6], [1, 3]]],
|
||||||
|
["0_____1", [[0, 7], [1, 3]]],
|
||||||
|
["0______1", [[0, 8], [1, 3]]],
|
||||||
|
["___01___", [[3, 5], [0, 2]]],
|
||||||
|
["____0_1", [[4, 7], [0, 2]]]],
|
||||||
|
FZF.new([]).sort_by_rank(xmatcher.match(list, '01 __', '', '')))
|
||||||
|
end
|
||||||
|
|
||||||
|
if RUBY_PLATFORM =~ /darwin/
|
||||||
|
NFD = '한글'
|
||||||
|
def test_nfc
|
||||||
|
assert_equal 6, NFD.length
|
||||||
|
assert_equal ["한글", [[0, 1], [1, 2]]],
|
||||||
|
FZF::UConv.nfc(NFD, [[0, 3], [3, 6]])
|
||||||
|
|
||||||
|
nfd2 = 'before' + NFD + 'after'
|
||||||
|
assert_equal 6 + 6 + 5, nfd2.length
|
||||||
|
|
||||||
|
nfc, offsets = FZF::UConv.nfc(nfd2, [[4, 14], [9, 13]])
|
||||||
|
o1, o2 = offsets
|
||||||
|
assert_equal 'before한글after', nfc
|
||||||
|
assert_equal 're한글af', nfc[(o1.first...o1.last)]
|
||||||
|
assert_equal '글a', nfc[(o2.first...o2.last)]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_nfd
|
||||||
|
nfc = '한글'
|
||||||
|
nfd = FZF::UConv.nfd(nfc)
|
||||||
|
assert_equal 6, nfd.length
|
||||||
|
assert_equal NFD, nfd
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user