diff --git a/README.md b/README.md index 75597f6..6254bff 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,9 @@ usage: fzf [options] -e, --extended-exact Extended-search mode (exact match) -i Case-insensitive match (default: smart-case match) +i Case-sensitive match - -n, --nth=[-]N[,..] Comma-separated list of field indexes for limiting - search scope (positive or negative integers) + -n, --nth=N[,..] Comma-separated list of field index expressions + for limiting search scope. Each can be a non-zero + integer or a range expression ([BEGIN]..[END]) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) Search result diff --git a/fzf b/fzf index bd08f24..6266461 100755 --- a/fzf +++ b/fzf @@ -7,7 +7,7 @@ # / __/ / /_/ __/ # /_/ /___/_/ Fuzzy finder for your shell # -# Version: 0.8.5 (Jun 12, 2014) +# Version: 0.8.5 (Jun 14, 2014) # # Author: Junegunn Choi # URL: https://github.com/junegunn/fzf @@ -133,10 +133,9 @@ class FZF 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 + usage 1, 'field expression required' unless nth = argv.shift @nth = parse_nth nth - when /^-n([0-9,-]+)$/, /^--nth=([0-9,-]+)$/ + when /^-n([0-9,-\.]+)$/, /^--nth=([0-9,-\.]+)$/ @nth = parse_nth $1 when '-d', '--delimiter' usage 1, 'delimiter required' unless delim = argv.shift @@ -179,10 +178,20 @@ class FZF end def parse_nth nth - nth.split(',').map { |n| - ni = n.to_i - usage 1, "invalid field number: #{n}" if ni == 0 - ni + nth.split(',').map { |expr| + x = proc { usage 1, "invalid field expression: #{expr}" } + first, second = expr.split('..', 2) + x.call if !first.empty? && first.to_i == 0 || + second && !second.empty? && (second.to_i == 0 || second.include?('.')) + + first = first.empty? ? 1 : first.to_i + second = case second + when nil then first + when '' then -1 + else second.to_i + end + + Range.new(*[first, second].map { |e| e > 0 ? e - 1 : e }) } end @@ -281,8 +290,9 @@ class FZF -e, --extended-exact Extended-search mode (exact match) -i Case-insensitive match (default: smart-case match) +i Case-sensitive match - -n, --nth=[-]N[,..] Comma-separated list of field indexes for limiting - search scope (positive or negative integers) + -n, --nth=N[,..] Comma-separated list of field index expressions + for limiting search scope. Each can be a non-zero + integer or a range expression ([BEGIN]..[END]) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) Search result @@ -1011,7 +1021,7 @@ class FZF end def initialize nth, delim - @nth = nth && nth.map { |n| n > 0 ? n - 1 : n } + @nth = nth @delim = delim @tokens_cache = {} end @@ -1033,8 +1043,9 @@ class FZF prefix_length, tokens = tokenize str @nth.each do |n| - if (token = tokens[n]) && (md = token.match(pat) rescue nil) - prefix_length += (tokens[0...n] || []).join.length + if (range = tokens[n]) && (token = range.join) && + (md = token.sub(/\s+$/, '').match(pat) rescue nil) + prefix_length += (tokens[0...(n.begin)] || []).join.length offset = md.offset(0).map { |o| o + prefix_length } return MatchData.new(offset) end diff --git a/install b/install index cee9e6b..9ed83b1 100755 --- a/install +++ b/install @@ -140,7 +140,7 @@ if [ -z "$(set -o | grep '^vi.*on')" ]; then fi # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s | sed \"s/ *[0-9]* *//\")\e\C-e\er"' + bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s +m -n..,1,2.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"' # ALT-C - cd into the selected directory bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' @@ -157,7 +157,7 @@ else fi # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' + bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s +m -n..,1,2.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' # ALT-C - cd into the selected directory bind '"\ec": "\eddi$(__fcd)\C-x\C-e\C-x\C-r\C-m"' @@ -216,7 +216,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { - LBUFFER=$(fc -l 1 | fzf +s | sed "s/ *[0-9*]* *//") + LBUFFER=$(fc -l 1 | fzf +s +m -n..,1,2.. | sed "s/ *[0-9*]* *//") zle redisplay } zle -N fzf-history-widget diff --git a/test/test_fzf.rb b/test/test_fzf.rb index c085d69..4c82104 100644 --- a/test/test_fzf.rb +++ b/test/test_fzf.rb @@ -60,7 +60,7 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal true, fzf.select1 assert_equal true, fzf.exit0 assert_equal true, fzf.reverse - assert_equal [3, -1, 2], fzf.nth + assert_equal [2..2, -1..-1, 1..1], fzf.nth end def test_option_parser @@ -80,7 +80,7 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal true, fzf.exit0 assert_equal 'howdy', fzf.filter assert_equal :exact, fzf.extended - assert_equal [1], fzf.nth + assert_equal [0..0], fzf.nth assert_equal true, fzf.reverse # Long opts (left-to-right) @@ -101,7 +101,7 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal false, fzf.select1 assert_equal false, fzf.exit0 assert_equal nil, fzf.extended - assert_equal [-2], fzf.nth + assert_equal [-2..-2], fzf.nth assert_equal false, fzf.reverse # Short opts @@ -114,7 +114,7 @@ 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 + assert_equal [2..2], fzf.nth assert_equal true, fzf.select1 assert_equal true, fzf.exit0 @@ -134,23 +134,28 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal false, fzf.exit0 assert_equal 'world', fzf.filter assert_equal nil, fzf.extended - assert_equal [4, 5], fzf.nth + assert_equal [3..3, 4..4], fzf.nth rescue SystemExit => e assert false, "Exited" end def test_invalid_option - [%w[--unknown], %w[yo dawg]].each do |argv| + [ + %w[--unknown], + %w[yo dawg], + %w[--nth=0], + %w[-n 0], + %w[-n 1..2..3], + %w[-n 1....], + %w[-n ....3], + %w[-n 1....3], + %w[-n 1..0], + %w[--nth ..0], + ].each do |argv| assert_raises(SystemExit) do 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 @@ -487,43 +492,67 @@ class TestFZF < MiniTest::Unit::TestCase [list[0], [[2, 5]]], [list[1], [[9, 17]]]], matcher.match(list, 'is', '', '') - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2] + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1] 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] + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2..2] assert_equal [[list[0], [[19, 20]]]], matcher.match(list, 'r', '', '') # Comma-separated - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [3, 1] + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2..2, 0..0] assert_equal [[list[0], [[19, 20]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '') # Ordered - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1, 3] + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0, 2..2] assert_equal [[list[0], [[3, 4]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '') regex = FZF.build_delim_regex "\t" - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1], regex + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0], regex assert_equal [[list[0], [[3, 10]]]], matcher.match(list, 're', '', '') - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2], regex + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], 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 + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [-1..-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 + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0], regex assert_equal [list[1]], matcher.match(list, 'f', '', '').map(&:first) - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2], regex + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], regex assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '') end + def test_nth_match_range + list = [ + ' first second third', + 'fourth fifth sixth', + ] + + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..2] + assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '') + assert_equal [], matcher.match(list, 'fo', '', '') + + matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..-1, 0..0] + assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '') + assert_equal [[list[1], [[0, 2]]]], matcher.match(list, 'fo', '', '') + + matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..0, 1..2] + assert_equal [], matcher.match(list, '^t', '', '') + + matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..1, 2..2] + assert_equal [[list[0], [[16, 17]]]], matcher.match(list, '^t', '', '') + + matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [1..-1] + assert_equal [[list[0], [[8, 9]]]], matcher.match(list, '^s', '', '') + end + def stream_for str StringIO.new(str).tap do |sio| sio.instance_eval do