From 76a3ef8c37bd6b853f79e022d6d4eae35f41d1ba Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 1 Nov 2014 13:46:24 +0900 Subject: [PATCH] Add --with-nth option (#102) --- .travis.yml | 9 ++++ README.md | 49 ++++++++--------- Rakefile | 1 + fzf | 137 +++++++++++++++++++++++++++++------------------ test/test_fzf.rb | 34 +++++++++++- 5 files changed, 151 insertions(+), 79 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f4e97df --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: ruby +rvm: + - "1.8.7" + - "1.9.3" + - "2.0.0" + - "2.1.1" + +install: gem install curses minitest + diff --git a/README.md b/README.md index 679c8f1..0b52864 100644 --- a/README.md +++ b/README.md @@ -65,38 +65,39 @@ Usage usage: fzf [options] Search - -x, --extended Extended-search mode - -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 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) + -x, --extended Extended-search mode + -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 index expressions + for limiting search scope. Each can be a non-zero + integer or a range expression ([BEGIN]..[END]) + --with-nth=N[,..] Transform the item using index expressions for search + -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) Search result - -s, --sort=MAX Maximum number of matched items to sort (default: 1000) - +s, --no-sort Do not sort the result. Keep the sequence unchanged. + -s, --sort=MAX Maximum number of matched items to sort (default: 1000) + +s, --no-sort Do not sort the result. Keep the sequence unchanged. Interface - -m, --multi Enable multi-select with tab/shift-tab - --no-mouse Disable mouse - +c, --no-color Disable colors - +2, --no-256 Disable 256-color - --black Use black background - --reverse Reverse orientation - --prompt=STR Input prompt (default: '> ') + -m, --multi Enable multi-select with tab/shift-tab + --no-mouse Disable mouse + +c, --no-color Disable colors + +2, --no-256 Disable 256-color + --black Use black background + --reverse Reverse orientation + --prompt=STR Input prompt (default: '> ') Scripting - -q, --query=STR Start the finder with the given query - -1, --select-1 Automatically select the only match - -0, --exit-0 Exit immediately when there's no match - -f, --filter=STR Filter mode. Do not start interactive finder. - --print-query Print query as the first line + -q, --query=STR Start the finder with the given query + -1, --select-1 Automatically select the only match + -0, --exit-0 Exit immediately when there's no match + -f, --filter=STR Filter mode. Do not start interactive finder. + --print-query Print query as the first line Environment variables - FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000") + FZF_DEFAULT_COMMAND Default command to use when input is tty + FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000") ``` fzf will launch curses-based finder, read the list from STDIN, and write the diff --git a/Rakefile b/Rakefile index 1c999e7..933a039 100644 --- a/Rakefile +++ b/Rakefile @@ -6,3 +6,4 @@ Rake::TestTask.new(:test) do |test| test.verbose = true end +task :default => :test diff --git a/fzf b/fzf index 0345412..2304958 100755 --- a/fzf +++ b/fzf @@ -7,7 +7,7 @@ # / __/ / /_/ __/ # /_/ /___/_/ Fuzzy finder for your shell # -# Version: 0.8.7 (Aug 17, 2014) +# Version: 0.8.8 (Nov 1, 2014) # # Author: Junegunn Choi # URL: https://github.com/junegunn/fzf @@ -53,11 +53,34 @@ unless String.method_defined? :force_encoding end end +class String + attr_accessor :orig + + def tokenize delim, nth + unless delim + # AWK default + prefix_length = (index(/\S/) || 0) rescue 0 + tokens = scan(/\S+\s*/) rescue [] + else + prefix_length = 0 + tokens = scan(delim) rescue [] + end + nth.map { |n| + if n.begin == 0 && n.end == -1 + [prefix_length, tokens.join] + elsif part = tokens[n] + [prefix_length + (tokens[0...(n.begin)] || []).join.length, + part.join] + end + }.compact + end +end + class FZF C = Curses attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256, :reverse, :prompt, :mouse, :multi, :query, :select1, :exit0, :filter, :extended, - :print_query + :print_query, :with_nth def sync @shr_mtx.synchronize { yield } @@ -95,6 +118,7 @@ class FZF @exit0 = false @filter = nil @nth = nil + @with_nth = nil @delim = nil @reverse = false @prompt = '> ' @@ -148,6 +172,11 @@ class FZF @nth = parse_nth nth when /^-n([0-9,-\.]+)$/, /^--nth=([0-9,-\.]+)$/ @nth = parse_nth $1 + when '--with-nth' + usage 1, 'field expression required' unless nth = argv.shift + @with_nth = parse_nth nth + when /^--with-nth=([0-9,-\.]+)$/ + @with_nth = parse_nth $1 when '-d', '--delimiter' usage 1, 'delimiter required' unless delim = argv.shift @delim = FZF.build_delim_regex delim @@ -181,6 +210,7 @@ class FZF @queue = Queue.new @pending = nil @rev_dir = @reverse ? -1 : 1 + @stdout = $stdout.clone unless @filter # Shared variables: needs protection @@ -200,7 +230,7 @@ class FZF end def parse_nth nth - nth.split(',').map { |expr| + ranges = 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 || @@ -215,6 +245,7 @@ class FZF Range.new(*[first, second].map { |e| e > 0 ? e - 1 : e }) } + ranges == [0..-1] ? nil : ranges end def FZF.build_delim_regex delim @@ -222,6 +253,10 @@ class FZF Regexp.compile "(?:.*?#{delim})|(?:.+?$)" end + def burp string + @stdout.puts(string.orig || string) + end + def start if @filter start_reader.join @@ -236,7 +271,7 @@ class FZF if loaded if @select1 && len == 1 puts @query if @print_query - puts empty ? matches.first : matches.first.first + burp(empty ? matches.first : matches.first.first) exit 0 elsif @exit0 && len == 0 puts @query if @print_query @@ -312,39 +347,40 @@ class FZF $stderr.puts %[usage: fzf [options] Search - -x, --extended Extended-search mode - -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 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) + -x, --extended Extended-search mode + -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 index expressions + for limiting search scope. Each can be a non-zero + integer or a range expression ([BEGIN]..[END]) + --with-nth=N[,..] Transform the item using index expressions for search + -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) Search result - -s, --sort=MAX Maximum number of matched items to sort (default: 1000) - +s, --no-sort Do not sort the result. Keep the sequence unchanged. + -s, --sort=MAX Maximum number of matched items to sort (default: 1000) + +s, --no-sort Do not sort the result. Keep the sequence unchanged. Interface - -m, --multi Enable multi-select with tab/shift-tab - --no-mouse Disable mouse - +c, --no-color Disable colors - +2, --no-256 Disable 256-color - --black Use black background - --reverse Reverse orientation - --prompt=STR Input prompt (default: '> ') + -m, --multi Enable multi-select with tab/shift-tab + --no-mouse Disable mouse + +c, --no-color Disable colors + +2, --no-256 Disable 256-color + --black Use black background + --reverse Reverse orientation + --prompt=STR Input prompt (default: '> ') Scripting - -q, --query=STR Start the finder with the given query - -1, --select-1 Automatically select the only match - -0, --exit-0 Exit immediately when there's no match - -f, --filter=STR Filter mode. Do not start interactive finder. - --print-query Print query as the first line + -q, --query=STR Start the finder with the given query + -1, --select-1 Automatically select the only match + -0, --exit-0 Exit immediately when there's no match + -f, --filter=STR Filter mode. Do not start interactive finder. + --print-query Print query as the first line Environment variables - FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000")] + $/ + $/ - exit x + FZF_DEFAULT_COMMAND Default command to use when input is tty + FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000")] + $/ + $/ + exit x end def emit event @@ -520,7 +556,6 @@ class FZF end def init_screen - @stdout = $stdout.clone $stdout.reopen($stderr) C.init_screen @@ -595,14 +630,28 @@ class FZF end Thread.new do - while line = stream.gets - emit(:new) { @new << line.chomp } + if @with_nth + while line = stream.gets + emit(:new) { @new << transform(line) } + end + else + while line = stream.gets + emit(:new) { @new << line.chomp } + end end emit(:loaded) { true } @spinner.clear if @spinner end end + def transform line + line = line.chomp + mut = (line =~ / $/ ? line : line + ' '). + tokenize(@delim, @with_nth).map { |e| e.last }.join('').sub(/ *$/, '') + mut.orig = line + mut + end + def start_search &callback Thread.new do lists = [] @@ -1080,10 +1129,10 @@ class FZF @stdout.puts q if @print_query if got if selects.empty? - @stdout.puts got + burp got else selects.each do |sel, _| - @stdout.puts sel + burp sel end end end @@ -1108,25 +1157,7 @@ class FZF end def tokenize str - @tokens_cache[str] ||= - begin - unless @delim - # AWK default - prefix_length = (str.index(/\S/) || 0) rescue 0 - tokens = str.scan(/\S+\s*/) rescue [] - else - prefix_length = 0 - tokens = str.scan(@delim) rescue [] - end - @nth.map { |n| - if n.begin == 0 && n.end == -1 - [prefix_length, tokens.join] - elsif part = tokens[n] - [prefix_length + (tokens[0...(n.begin)] || []).join.length, - part.join] - end - }.compact - end + @tokens_cache[str] ||= str.tokenize(@delim, @nth) end def do_match str, pat diff --git a/test/test_fzf.rb b/test/test_fzf.rb index 806df8a..6fbd426 100644 --- a/test/test_fzf.rb +++ b/test/test_fzf.rb @@ -1,6 +1,7 @@ #!/usr/bin/env ruby # encoding: utf-8 +require 'rubygems' require 'curses' require 'timeout' require 'stringio' @@ -25,6 +26,7 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal nil, fzf.rxflag assert_equal true, fzf.mouse assert_equal nil, fzf.nth + assert_equal nil, fzf.with_nth assert_equal true, fzf.color assert_equal false, fzf.black assert_equal true, fzf.ansi256 @@ -47,7 +49,7 @@ class TestFZF < MiniTest::Unit::TestCase ENV['FZF_DEFAULT_OPTS'] = '-x -m -s 10000 -q " hello world " +c +2 --select-1 -0 ' << - '--no-mouse -f "goodbye world" --black --nth=3,-1,2 --reverse --print-query' + '--no-mouse -f "goodbye world" --black --with-nth=3,-3..,2 --nth=3,-1,2 --reverse --print-query' fzf = FZF.new [] assert_equal 10000, fzf.sort assert_equal ' hello world ', @@ -65,13 +67,14 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal true, fzf.reverse assert_equal true, fzf.print_query assert_equal [2..2, -1..-1, 1..1], fzf.nth + assert_equal [2..2, -3..-1, 1..1], fzf.with_nth end def test_option_parser # Long opts fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --select-1 --exit-0 --filter=howdy --extended-exact - --no-mouse --no-256 --nth=1 --reverse --prompt (hi) + --no-mouse --no-256 --nth=1 --with-nth=.. --reverse --prompt (hi) --print-query] assert_equal 2000, fzf.sort assert_equal true, fzf.multi @@ -86,6 +89,7 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal 'howdy', fzf.filter assert_equal :exact, fzf.extended assert_equal [0..0], fzf.nth + assert_equal nil, fzf.with_nth assert_equal true, fzf.reverse assert_equal '(hi)', fzf.prompt assert_equal true, fzf.print_query @@ -638,6 +642,32 @@ class TestFZF < MiniTest::Unit::TestCase assert_fzf_output %w[--exit-0], '', '' end + def test_with_nth + source = "hello world\nbatman" + assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q ^worl], + source, 'hello world' + assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q llo$], + source, 'hello world' + assert_fzf_output %w[-0 -1 --with-nth=.. -x -q llo$], + source, '' + assert_fzf_output %w[-0 -1 --with-nth=2,2,2,..,1 -x -q worlworlworlhellworlhell], + source, 'hello world' + assert_fzf_output %w[-0 -1 --with-nth=1,1,-1,1 -x -q batbatbatbat], + source, 'batman' + end + + def test_with_nth_transform + fzf = FZF.new %w[--with-nth 2..,1] + assert_equal 'my world hello', fzf.transform('hello my world') + assert_equal 'my world hello', fzf.transform('hello my world') + assert_equal 'my world hello', fzf.transform('hello my world ') + + fzf = FZF.new %w[--with-nth 2,-1,2] + assert_equal 'my world my', fzf.transform('hello my world') + assert_equal 'world world world', fzf.transform('hello world') + assert_equal 'world world world', fzf.transform('hello world ') + end + def test_ranking_overlap_match_regions list = [ '1 3 4 2',