diff --git a/README.md b/README.md index f0bd85d..624399c 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ usage: fzf [options] -e, --extended-exact Extended-search mode (exact match) -q, --query=STR Initial query -f, --filter=STR Filter mode. Do not start interactive finder. + -n, --nth=[-]N Match only in the N-th token of the item + -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -s, --sort=MAX Maximum number of matched items to sort (default: 1000) +s, --no-sort Do not sort the result. Keep the sequence unchanged. -i Case-insensitive match (default: smart-case match) @@ -177,6 +179,14 @@ fco() { git checkout $(echo "$commit" | sed "s/ .*//") } +# ftags - search ctags +ftags() { + local line + [ -e tags ] && + line=$(grep -v "^!" tags | cut -f1-3 | cut -c1-80 | fzf --nth=1) && + $EDITOR $(cut -f2 <<< "$line") +} + # fq1 [QUERY] # - Immediately select the file when there's only one match. # If not, start the fuzzy finder as usual. diff --git a/fzf b/fzf index 5c3d50f..a623e00 100755 --- a/fzf +++ b/fzf @@ -7,7 +7,7 @@ # / __/ / /_/ __/ # /_/ /___/_/ Fuzzy finder for your shell # -# Version: 0.8.2 (March 15, 2014) +# Version: 0.8.2 (March 30, 2014) # # Author: Junegunn Choi # URL: https://github.com/junegunn/fzf @@ -50,7 +50,8 @@ end class FZF C = Curses - attr_reader :rxflag, :sort, :color, :black, :ansi256, :mouse, :multi, :query, :filter, :extended + attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256, + :mouse, :multi, :query, :filter, :extended class AtomicVar def initialize value @@ -83,6 +84,8 @@ class FZF @mouse = true @extended = nil @filter = nil + @nth = nil + @delim = nil argv = if opts = ENV['FZF_DEFAULT_OPTS'] @@ -120,6 +123,17 @@ class FZF @filter = query when /^-f(.*)$/, /^--filter=(.*)$/ @filter = $1 + when '-n', '--nth' + usage 1, 'field number required' unless nth = argv.shift + usage 1, 'invalid field number' if nth.to_i == 0 + @nth = nth.to_i + when /^-n(-?[1-9][0-9]*)$/, /^--nth=(-?[1-9][0-9]*)$/ + @nth = $1.to_i + when '-d', '--delimiter' + usage 1, 'delimiter required' unless delim = argv.shift + @delim = FZF.build_delim_regex delim + when /^-d(.+)$/, /^--delimiter=(.+)$/ + @delim = FZF.build_delim_regex $1 when '-s', '--sort' usage 1, 'sort size required' unless sort = argv.shift usage 1, 'invalid sort size' unless sort =~ /^[0-9]+$/ @@ -155,6 +169,11 @@ class FZF end end + def FZF.build_delim_regex delim + Regexp.compile(delim) rescue (delim = Regexp.escape(delim)) + Regexp.compile "(?:.*?#{delim})|(?:.+?$)" + end + def start if @filter start_reader(false).join @@ -181,9 +200,9 @@ class FZF def get_matcher if @extended - ExtendedFuzzyMatcher.new @rxflag, @extended + ExtendedFuzzyMatcher.new @rxflag, @extended, @nth, @delim else - FuzzyMatcher.new @rxflag + FuzzyMatcher.new @rxflag, @nth, @delim end end @@ -208,6 +227,8 @@ class FZF -e, --extended-exact Extended-search mode (exact match) -q, --query=STR Initial query -f, --filter=STR Filter mode. Do not start interactive finder. + -n, --nth=[-]N Match only in the N-th token of the item + -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -s, --sort=MAX Maximum number of matched items to sort (default: 1000) +s, --no-sort Do not sort the result. Keep the sequence unchanged. -i Case-insensitive match (default: smart-case match) @@ -1026,10 +1047,55 @@ class FZF end end + class Matcher + class MatchData + def initialize n + @n = n + end + + def offset _ + @n + end + end + + def initialize nth, delim + @nth = nth && (nth > 0 ? nth - 1 : nth) + @delim = delim + @tokens_cache = {} + end + + def tokenize str + @tokens_cache[str] ||= + unless @delim + # AWK default + prefix_length = str[/^\s+/].length rescue 0 + [prefix_length, (str.strip.scan(/\S+\s*/) rescue [])] + else + prefix_length = 0 + [prefix_length, (str.scan(@delim) rescue [])] + end + end + + def do_match str, pat + if @nth + prefix_length, tokens = tokenize str + + if (token = tokens[@nth]) && (md = token.match(pat) rescue nil) + prefix_length += (tokens[0...@nth] || []).join.length + offset = md.offset(0).map { |o| o + prefix_length } + MatchData.new offset + end + else + str.match(pat) rescue nil + end + end + end + class FuzzyMatcher < Matcher attr_reader :caches, :rxflag - def initialize rxflag + def initialize rxflag, nth = nil, delim = nil + super nth, delim @caches = Hash.new { |h, k| h[k] = {} } @regexp = {} @rxflag = rxflag @@ -1073,15 +1139,15 @@ class FZF cache[q] ||= (partial_cache ? partial_cache.map { |e| e.first } : list).map { |line| # Ignore errors: e.g. invalid byte sequence in UTF-8 - md = line.match(regexp) rescue nil + md = do_match(line, regexp) md && [line, [md.offset(0)]] }.compact end end class ExtendedFuzzyMatcher < FuzzyMatcher - def initialize rxflag, mode = :fuzzy - super rxflag + def initialize rxflag, mode = :fuzzy, nth = nil, delim = nil + super rxflag, nth, delim @regexps = {} @mode = mode end @@ -1143,7 +1209,7 @@ class FZF offsets = [] regexps.all? { |pair| regexp, invert = pair - md = line.match(regexp) rescue nil + md = do_match(line, regexp) if md && !invert offsets << md.offset(0) elsif !md && invert diff --git a/test/test_fzf.rb b/test/test_fzf.rb index b236d6e..c9ac904 100644 --- a/test/test_fzf.rb +++ b/test/test_fzf.rb @@ -27,8 +27,10 @@ class TestFZF < MiniTest::Unit::TestCase ENV['FZF_DEFAULT_SORT'] = '20000' fzf = FZF.new [] assert_equal 20000, fzf.sort + assert_equal nil, fzf.nth - ENV['FZF_DEFAULT_OPTS'] = '-x -m -s 10000 -q " hello world " +c +2 --no-mouse -f "goodbye world" --black' + ENV['FZF_DEFAULT_OPTS'] = + '-x -m -s 10000 -q " hello world " +c +2 --no-mouse -f "goodbye world" --black --nth=3' fzf = FZF.new [] assert_equal 10000, fzf.sort assert_equal ' hello world ', @@ -41,12 +43,13 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal false, fzf.ansi256 assert_equal true, fzf.black assert_equal false, fzf.mouse + assert_equal 3, fzf.nth end def test_option_parser # Long opts fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello - --filter=howdy --extended-exact --no-mouse --no-256] + --filter=howdy --extended-exact --no-mouse --no-256 --nth=1] assert_equal 2000, fzf.sort assert_equal true, fzf.multi assert_equal false, fzf.color @@ -57,9 +60,10 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal 'hello', fzf.query.get assert_equal 'howdy', fzf.filter assert_equal :exact, fzf.extended + assert_equal 1, fzf.nth fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello - --filter a --filter b --no-256 --black + --filter a --filter b --no-256 --black --nth 2 --no-sort -i --color --no-multi --256] assert_equal nil, fzf.sort assert_equal false, fzf.multi @@ -71,9 +75,10 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal 'b', fzf.filter assert_equal 'hello', fzf.query.get assert_equal nil, fzf.extended + assert_equal 2, fzf.nth # Short opts - fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fhowdy +2] + fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fhowdy +2 -n3] assert_equal 2000, fzf.sort assert_equal true, fzf.multi assert_equal false, fzf.color @@ -82,9 +87,10 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal 'hello', fzf.query.get assert_equal 'howdy', fzf.filter assert_equal :fuzzy, fzf.extended + assert_equal 3, fzf.nth # Left-to-right - fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fgoodbye +2 + fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fgoodbye +2 -n3 -n4 -s 3000 -c +m -i -q world +x -fworld -2 --black --no-black] assert_equal 3000, fzf.sort assert_equal false, fzf.multi @@ -95,6 +101,7 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal 'world', fzf.query.get assert_equal 'world', fzf.filter assert_equal nil, fzf.extended + assert_equal 4, fzf.nth fzf = FZF.new %w[--query hello +s -s 2000 --query=world] assert_equal 2000, fzf.sort @@ -109,6 +116,12 @@ class TestFZF < MiniTest::Unit::TestCase fzf = FZF.new argv end end + assert_raises(SystemExit) do + fzf = FZF.new %w[--nth=0] + end + assert_raises(SystemExit) do + fzf = FZF.new %w[-n 0] + end end # FIXME Only on 1.9 or above @@ -476,5 +489,46 @@ class TestFZF < MiniTest::Unit::TestCase sleep interval assert_equal false, me.double?(20) end + + def test_nth_match + list = [ + ' first second third', + 'fourth fifth sixth', + ] + + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE + assert_equal list, matcher.match(list, 'f', '', '').map(&:first) + assert_equal [ + [list[0], [[2, 5]]], + [list[1], [[9, 17]]]], matcher.match(list, 'is', '', '') + + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, 2 + assert_equal [[list[1], [[8, 9]]]], matcher.match(list, 'f', '', '') + assert_equal [[list[0], [[8, 9]]]], matcher.match(list, 's', '', '') + + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, 3 + assert_equal [[list[0], [[19, 20]]]], matcher.match(list, 'r', '', '') + + regex = FZF.build_delim_regex "\t" + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, 1, regex + assert_equal [[list[0], [[3, 10]]]], matcher.match(list, 're', '', '') + + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, 2, regex + assert_equal [], matcher.match(list, 'r', '', '') + assert_equal [[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '') + + # Negative indexing + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, -1, regex + assert_equal [[list[0], [[3, 6]]]], matcher.match(list, 'rt', '', '') + assert_equal [[list[0], [[2, 5]]], [list[1], [[9, 17]]]], matcher.match(list, 'is', '', '') + + # Regex delimiter + regex = FZF.build_delim_regex "[ \t]+" + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, 1, regex + assert_equal [list[1]], matcher.match(list, 'f', '', '').map(&:first) + + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, 2, regex + assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '') + end end