diff options
author | Junegunn Choi <junegunn.c@gmail.com> | 2014-11-04 23:30:11 +0900 |
---|---|---|
committer | Junegunn Choi <junegunn.c@gmail.com> | 2014-11-04 23:30:11 +0900 |
commit | 80819f3c442dc06cd0e1ce07dca0ebe62b24f67e (patch) | |
tree | 53c5022cc3763215c201cb5118403376b92512a1 | |
parent | 6fd6fff3a65ec23cfd84d347d77648fde2b24ee9 (diff) | |
parent | 7571baadb4c56b1d4bee8732b98c9ca7f3948f67 (diff) |
Merge pull request #104 from junegunn/add-with-nth0.8.8
Add --with-nth option
-rw-r--r-- | .travis.yml | 9 | ||||
-rw-r--r-- | README.md | 49 | ||||
-rw-r--r-- | Rakefile | 1 | ||||
-rwxr-xr-x | fzf | 144 | ||||
-rw-r--r-- | test/test_fzf.rb | 144 |
5 files changed, 250 insertions, 97 deletions
diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..f4e97dff --- /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 + @@ -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 @@ -6,3 +6,4 @@ Rake::TestTask.new(:test) do |test| test.verbose = true end +task :default => :test @@ -7,7 +7,7 @@ # / __/ / /_/ __/ # /_/ /___/_/ Fuzzy finder for your shell # -# Version: 0.8.7 (Aug 17, 2014) +# Version: 0.8.8 (Nov 4, 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, orig = nil + @stdout.puts(orig || 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 = [] @@ -694,7 +743,8 @@ class FZF def pick sync do - [*@matches.fetch(@ycur, [])][0] + item = @matches[@ycur] + item.is_a?(Array) ? item[0] : item end end @@ -1000,7 +1050,7 @@ class FZF if @selects.has_key? sel @selects.delete sel else - @selects[sel] = 1 + @selects[sel] = sel.orig end end vselect { |v| v + case o @@ -1080,10 +1130,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 + selects.each do |sel, orig| + burp sel, orig end end end @@ -1108,25 +1158,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 806df8a7..2bb7dce9 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' @@ -10,6 +11,48 @@ $LOAD_PATH.unshift File.expand_path('../..', __FILE__) ENV['FZF_EXECUTABLE'] = '0' load 'fzf' +class MockTTY + def initialize + @buffer = '' + @mutex = Mutex.new + @condv = ConditionVariable.new + end + + def read_nonblock sz + @mutex.synchronize do + take sz + end + end + + def take sz + if @buffer.length >= sz + ret = @buffer[0, sz] + @buffer = @buffer[sz..-1] + ret + end + end + + def getc + sleep 0.1 + while true + @mutex.synchronize do + if char = take(1) + return char + else + @condv.wait(@mutex) + end + end + end + end + + def << str + @mutex.synchronize do + @buffer << str + @condv.broadcast + end + end +end + class TestFZF < MiniTest::Unit::TestCase def setup ENV.delete 'FZF_DEFAULT_SORT' @@ -25,6 +68,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 +91,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 +109,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 +131,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 @@ -168,13 +214,12 @@ class TestFZF < MiniTest::Unit::TestCase end end - # FIXME Only on 1.9 or above def test_width fzf = FZF.new [] assert_equal 5, fzf.width('abcde') assert_equal 4, fzf.width('한글') assert_equal 5, fzf.width('한글.') - end + end if RUBY_VERSION >= '1.9' def test_trim fzf = FZF.new [] @@ -187,7 +232,7 @@ class TestFZF < MiniTest::Unit::TestCase 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 if RUBY_VERSION >= '1.9' def test_format fzf = FZF.new [] @@ -563,28 +608,41 @@ class TestFZF < MiniTest::Unit::TestCase assert_equal [[list[0], [[8, 9]]]], matcher.match(list, '^s', '', '') end - def stream_for str + def stream_for str, delay = 0 StringIO.new(str).tap do |sio| sio.instance_eval do alias org_gets gets def gets - org_gets.tap { |e| sleep 0.5 unless e.nil? } + org_gets.tap { |e| sleep(@delay) unless e.nil? } + end + + def reopen _ end end + sio.instance_variable_set :@delay, delay end end def assert_fzf_output opts, given, expected stream = stream_for given - output = StringIO.new + output = stream_for '' + + def sorted_lines line + line.split($/).sort + end begin + tty = MockTTY.new $stdout = output - FZF.new(opts, stream).start + fzf = FZF.new(opts, stream) + fzf.instance_variable_set :@tty, tty + thr = block_given? && Thread.new { yield tty } + fzf.start + thr && thr.join rescue SystemExit => e assert_equal 0, e.status - assert_equal expected, output.string.chomp + assert_equal sorted_lines(expected), sorted_lines(output.string) ensure $stdout = STDOUT end @@ -613,15 +671,12 @@ class TestFZF < MiniTest::Unit::TestCase end def test_select_1_ambiguity - stream = stream_for "Hello\nWorld" begin - Timeout::timeout(2) do - FZF.new(%w[--query=o --select-1], stream).start + Timeout::timeout(0.5) do + assert_fzf_output %w[--query=o --select-1], "hello\nworld", "should not match" end - flunk 'Should not reach here' - rescue Exception => e + rescue Timeout::Error Curses.close_screen - assert_instance_of Timeout::Error, e end end @@ -638,6 +693,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', @@ -688,13 +769,42 @@ class TestFZF < MiniTest::Unit::TestCase tmp << 'hello ' << [0xff].pack('C*') << ' world' << $/ << [0xff].pack('C*') tmp.close begin - Timeout::timeout(1) do + Timeout::timeout(0.5) do FZF.new(%w[-n..,1,2.. -q^ -x], File.open(tmp.path)).start end rescue Timeout::Error + Curses.close_screen end ensure tmp.unlink end + + def test_with_nth_mock_tty + # Manual selection with input + assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty| + tty << "world" + tty << "hell" + tty << "\r" + end + + # Manual selection without input + assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty| + tty << "\r" + end + + # Manual selection with input and --multi + lines = "hello world\ngoodbye world" + assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty| + tty << "o" + tty << "\e[Z\e[Z" + tty << "\r" + end + + # Manual selection without input and --multi + assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty| + tty << "\e[Z\e[Z" + tty << "\r" + end + end end |