mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2025-01-09 17:53:56 +00:00
parent
ab9fbf1967
commit
22d3929ae3
53
README.md
53
README.md
@ -50,23 +50,31 @@ Usage
|
||||
```
|
||||
usage: fzf [options]
|
||||
|
||||
Options
|
||||
-m, --multi Enable multi-select
|
||||
Search
|
||||
-x, --extended Extended-search mode
|
||||
-e, --extended-exact Extended-search mode (exact match)
|
||||
-q, --query=STR Initial query
|
||||
-f, --filter=STR Filter mode. Do not start interactive finder.
|
||||
-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)
|
||||
-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.
|
||||
-i Case-insensitive match (default: smart-case match)
|
||||
+i Case-sensitive match
|
||||
|
||||
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
|
||||
--no-mouse Disable mouse
|
||||
|
||||
Scripting
|
||||
-q, --query=STR Start the finder with the given query
|
||||
-1, --select-1 (with --query) Automatically select the only match
|
||||
-0, --exit-0 (with --query) Exit when there's no match
|
||||
-f, --filter=STR Filter mode. Do not start interactive finder.
|
||||
|
||||
Environment variables
|
||||
FZF_DEFAULT_COMMAND Default command to use when input is tty
|
||||
@ -137,10 +145,12 @@ Useful examples
|
||||
---------------
|
||||
|
||||
```sh
|
||||
# vimf - Open selected file in Vim
|
||||
vimf() {
|
||||
# fe [FUZZY PATTERN] - Open the selected file with the default editor
|
||||
# - Bypass fuzzy finder if there's only one match (--select-1)
|
||||
# - Exit if there's no match (--exit-0)
|
||||
fe() {
|
||||
local file
|
||||
file=$(fzf --query="$1") && vim "$file"
|
||||
file=$(fzf --query="$1" --select-1 --exit-0) && ${EDITOR:-vim} "$file"
|
||||
}
|
||||
|
||||
# fd - cd to selected directory
|
||||
@ -193,29 +203,6 @@ ftags() {
|
||||
) && $EDITOR $(cut -f3 <<< "$line") -c "set nocst" \
|
||||
-c "silent tag $(cut -f2 <<< "$line")"
|
||||
}
|
||||
|
||||
# fq1 [QUERY]
|
||||
# - Immediately select the file when there's only one match.
|
||||
# If not, start the fuzzy finder as usual.
|
||||
fq1() {
|
||||
local lines
|
||||
lines=$(fzf --filter="$1" --no-sort)
|
||||
if [ -z "$lines" ]; then
|
||||
return 1
|
||||
elif [ $(wc -l <<< "$lines") -eq 1 ]; then
|
||||
echo "$lines"
|
||||
else
|
||||
echo "$lines" | fzf --query="$1"
|
||||
fi
|
||||
}
|
||||
|
||||
# fe [QUERY]
|
||||
# - Open the selected file with the default editor
|
||||
# (Bypass fuzzy finder when there's only one match)
|
||||
fe() {
|
||||
local file
|
||||
file=$(fq1 "$1") && ${EDITOR:-vim} "$file"
|
||||
}
|
||||
```
|
||||
|
||||
Key bindings for command line
|
||||
|
90
fzf
90
fzf
@ -51,7 +51,7 @@ end
|
||||
class FZF
|
||||
C = Curses
|
||||
attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256,
|
||||
:mouse, :multi, :query, :filter, :extended
|
||||
:mouse, :multi, :query, :select1, :exit0, :filter, :extended
|
||||
|
||||
class AtomicVar
|
||||
def initialize value
|
||||
@ -83,6 +83,8 @@ class FZF
|
||||
@multi = false
|
||||
@mouse = true
|
||||
@extended = nil
|
||||
@select1 = false
|
||||
@exit0 = false
|
||||
@filter = nil
|
||||
@nth = nil
|
||||
@delim = nil
|
||||
@ -113,6 +115,10 @@ class FZF
|
||||
when '--mouse' then @mouse = true
|
||||
when '--no-mouse' then @mouse = false
|
||||
when '+s', '--no-sort' then @sort = nil
|
||||
when '-1', '--select-1' then @select1 = true
|
||||
when '+1', '--no-select-1' then @select1 = false
|
||||
when '-0', '--exit-0' then @exit0 = true
|
||||
when '+0', '--no-exit-0' then @exit0 = false
|
||||
when '-q', '--query'
|
||||
usage 1, 'query string required' unless query = argv.shift
|
||||
@query = AtomicVar.new query.dup
|
||||
@ -184,29 +190,48 @@ class FZF
|
||||
|
||||
def start
|
||||
if @filter
|
||||
start_reader(false).join
|
||||
start_reader.join
|
||||
filter_list @new
|
||||
else
|
||||
@stdout = $stdout.clone
|
||||
$stdout.reopen($stderr)
|
||||
start_reader
|
||||
emit(:key) { q = @query.get; [q, q.length] } unless @query.empty?
|
||||
if !@query.empty? && (@select1 || @exit0)
|
||||
start_search do |loaded, matches|
|
||||
len = matches.length
|
||||
if loaded
|
||||
if @select1 && len == 1
|
||||
puts matches.first.first
|
||||
exit 0
|
||||
elsif @exit0 && len == 0
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
start_reader true
|
||||
init_screen
|
||||
if loaded || len > 1
|
||||
start_renderer
|
||||
Thread.new { start_loop }
|
||||
end
|
||||
end
|
||||
|
||||
sleep
|
||||
else
|
||||
start_search
|
||||
start_renderer
|
||||
start_loop
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def filter_list list
|
||||
matches = get_matcher.match(list, @filter, '', '')
|
||||
matches = matcher.match(list, @filter, '', '')
|
||||
if @sort && matches.length <= @sort
|
||||
matches = sort_by_rank(matches)
|
||||
end
|
||||
matches.each { |m| puts m.first }
|
||||
end
|
||||
|
||||
def get_matcher
|
||||
def matcher
|
||||
@matcher ||=
|
||||
if @extended
|
||||
ExtendedFuzzyMatcher.new @rxflag, @extended, @nth, @delim
|
||||
else
|
||||
@ -218,7 +243,7 @@ class FZF
|
||||
File.open(__FILE__, 'r') do |f|
|
||||
f.each_line do |line|
|
||||
if line =~ /Version: (.*)/
|
||||
$stdout.puts "fzf " << $1
|
||||
$stdout.puts 'fzf ' << $1
|
||||
exit
|
||||
end
|
||||
end
|
||||
@ -229,23 +254,31 @@ class FZF
|
||||
$stderr.puts message if message
|
||||
$stderr.puts %[usage: fzf [options]
|
||||
|
||||
Options
|
||||
-m, --multi Enable multi-select
|
||||
Search
|
||||
-x, --extended Extended-search mode
|
||||
-e, --extended-exact Extended-search mode (exact match)
|
||||
-q, --query=STR Initial query
|
||||
-f, --filter=STR Filter mode. Do not start interactive finder.
|
||||
-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)
|
||||
-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.
|
||||
-i Case-insensitive match (default: smart-case match)
|
||||
+i Case-sensitive match
|
||||
|
||||
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
|
||||
--no-mouse Disable mouse
|
||||
|
||||
Scripting
|
||||
-q, --query=STR Start the finder with the given query
|
||||
-1, --select-1 (with --query) Automatically select the only match
|
||||
-0, --exit-0 (with --query) Exit when there's no match
|
||||
-f, --filter=STR Filter mode. Do not start interactive finder.
|
||||
|
||||
Environment variables
|
||||
FZF_DEFAULT_COMMAND Default command to use when input is tty
|
||||
@ -547,6 +580,9 @@ class FZF
|
||||
end
|
||||
|
||||
def init_screen
|
||||
@stdout = $stdout.clone
|
||||
$stdout.reopen($stderr)
|
||||
|
||||
C.init_screen
|
||||
C.mousemask C::ALL_MOUSE_EVENTS if @mouse
|
||||
C.start_color
|
||||
@ -604,7 +640,7 @@ class FZF
|
||||
C.refresh
|
||||
end
|
||||
|
||||
def start_reader curses
|
||||
def start_reader
|
||||
stream =
|
||||
if @source.tty?
|
||||
if default_command = ENV['FZF_DEFAULT_COMMAND']
|
||||
@ -623,13 +659,12 @@ class FZF
|
||||
emit(:new) { @new << line.chomp }
|
||||
end
|
||||
emit(:loaded) { true }
|
||||
@spinner.clear if curses
|
||||
@spinner.clear if @spinner
|
||||
end
|
||||
end
|
||||
|
||||
def start_search
|
||||
matcher = get_matcher
|
||||
searcher = Thread.new {
|
||||
def start_search &callback
|
||||
Thread.new do
|
||||
lists = []
|
||||
events = {}
|
||||
fcache = {}
|
||||
@ -668,7 +703,7 @@ class FZF
|
||||
progress = 0
|
||||
started_at = Time.now
|
||||
|
||||
if new_search && !lists.empty?
|
||||
if updated = new_search && !lists.empty?
|
||||
q, cx = events.delete(:key) || [q, 0]
|
||||
empty = matcher.empty?(q)
|
||||
unless matches = fcache[q]
|
||||
@ -699,6 +734,10 @@ class FZF
|
||||
@matches.set matches
|
||||
end#new_search
|
||||
|
||||
callback = nil if callback &&
|
||||
(updated || events[:loaded]) &&
|
||||
callback.call(events[:loaded], matches)
|
||||
|
||||
# This small delay reduces the number of partial lists
|
||||
sleep((delay = [20, delay + 5].min) * 0.01) unless user_input
|
||||
|
||||
@ -707,7 +746,7 @@ class FZF
|
||||
rescue Exception => e
|
||||
@main.raise e
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def pick
|
||||
@ -751,6 +790,8 @@ class FZF
|
||||
end
|
||||
|
||||
def start_renderer
|
||||
init_screen
|
||||
|
||||
Thread.new do
|
||||
begin
|
||||
while blk = @queue.shift
|
||||
@ -1030,7 +1071,6 @@ class FZF
|
||||
actions[127] = actions[ctrl(:h)]
|
||||
actions[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc]
|
||||
|
||||
emit(:key) { [@query.get, cursor] } unless @query.empty?
|
||||
while true
|
||||
@cursor_x.set cursor
|
||||
render { print_input }
|
||||
@ -1093,7 +1133,7 @@ class FZF
|
||||
if (token = tokens[n]) && (md = token.match(pat) rescue nil)
|
||||
prefix_length += (tokens[0...n] || []).join.length
|
||||
offset = md.offset(0).map { |o| o + prefix_length }
|
||||
return MatchData.new offset
|
||||
return MatchData.new(offset)
|
||||
end
|
||||
end
|
||||
nil
|
||||
|
101
test/test_fzf.rb
101
test/test_fzf.rb
@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env ruby
|
||||
# encoding: utf-8
|
||||
|
||||
require 'curses'
|
||||
require 'timeout'
|
||||
require 'stringio'
|
||||
require 'minitest/autorun'
|
||||
$LOAD_PATH.unshift File.expand_path('../..', __FILE__)
|
||||
ENV['FZF_EXECUTABLE'] = '0'
|
||||
@ -20,6 +23,15 @@ class TestFZF < MiniTest::Unit::TestCase
|
||||
assert_equal true, fzf.color
|
||||
assert_equal nil, fzf.rxflag
|
||||
assert_equal true, fzf.mouse
|
||||
assert_equal nil, fzf.nth
|
||||
assert_equal true, fzf.color
|
||||
assert_equal false, fzf.black
|
||||
assert_equal true, fzf.ansi256
|
||||
assert_equal '', fzf.query.get
|
||||
assert_equal false, fzf.select1
|
||||
assert_equal false, fzf.exit0
|
||||
assert_equal nil, fzf.filter
|
||||
assert_equal nil, fzf.extended
|
||||
end
|
||||
|
||||
def test_environment_variables
|
||||
@ -30,7 +42,8 @@ class TestFZF < MiniTest::Unit::TestCase
|
||||
assert_equal nil, fzf.nth
|
||||
|
||||
ENV['FZF_DEFAULT_OPTS'] =
|
||||
'-x -m -s 10000 -q " hello world " +c +2 --no-mouse -f "goodbye world" --black --nth=3,-1,2'
|
||||
'-x -m -s 10000 -q " hello world " +c +2 --select-1 -0 ' +
|
||||
'--no-mouse -f "goodbye world" --black --nth=3,-1,2'
|
||||
fzf = FZF.new []
|
||||
assert_equal 10000, fzf.sort
|
||||
assert_equal ' hello world ',
|
||||
@ -43,13 +56,16 @@ class TestFZF < MiniTest::Unit::TestCase
|
||||
assert_equal false, fzf.ansi256
|
||||
assert_equal true, fzf.black
|
||||
assert_equal false, fzf.mouse
|
||||
assert_equal true, fzf.select1
|
||||
assert_equal true, fzf.exit0
|
||||
assert_equal [3, -1, 2], 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 --nth=1]
|
||||
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]
|
||||
assert_equal 2000, fzf.sort
|
||||
assert_equal true, fzf.multi
|
||||
assert_equal false, fzf.color
|
||||
@ -58,12 +74,16 @@ class TestFZF < MiniTest::Unit::TestCase
|
||||
assert_equal false, fzf.mouse
|
||||
assert_equal 0, fzf.rxflag
|
||||
assert_equal 'hello', fzf.query.get
|
||||
assert_equal true, fzf.select1
|
||||
assert_equal true, fzf.exit0
|
||||
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 --nth -2
|
||||
# Long opts (left-to-right)
|
||||
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query=hello
|
||||
--filter a --filter b --no-256 --black --nth -1 --nth -2
|
||||
--select-1 --exit-0 --no-select-1 --no-exit-0
|
||||
--no-sort -i --color --no-multi --256]
|
||||
assert_equal nil, fzf.sort
|
||||
assert_equal false, fzf.multi
|
||||
@ -74,11 +94,13 @@ class TestFZF < MiniTest::Unit::TestCase
|
||||
assert_equal 1, fzf.rxflag
|
||||
assert_equal 'b', fzf.filter
|
||||
assert_equal 'hello', fzf.query.get
|
||||
assert_equal false, fzf.select1
|
||||
assert_equal false, fzf.exit0
|
||||
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 -n3]
|
||||
fzf = FZF.new %w[-s2000 +c -m +i -qhello -x -fhowdy +2 -n3 -1 -0]
|
||||
assert_equal 2000, fzf.sort
|
||||
assert_equal true, fzf.multi
|
||||
assert_equal false, fzf.color
|
||||
@ -88,10 +110,14 @@ class TestFZF < MiniTest::Unit::TestCase
|
||||
assert_equal 'howdy', fzf.filter
|
||||
assert_equal :fuzzy, fzf.extended
|
||||
assert_equal [3], fzf.nth
|
||||
assert_equal true, fzf.select1
|
||||
assert_equal true, fzf.exit0
|
||||
|
||||
# Left-to-right
|
||||
fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fgoodbye +2 -n3 -n4,5
|
||||
-s 3000 -c +m -i -q world +x -fworld -2 --black --no-black]
|
||||
-s 3000 -c +m -i -q world +x -fworld -2 --black --no-black
|
||||
-1 -0 +1 +0
|
||||
]
|
||||
assert_equal 3000, fzf.sort
|
||||
assert_equal false, fzf.multi
|
||||
assert_equal true, fzf.color
|
||||
@ -99,13 +125,11 @@ class TestFZF < MiniTest::Unit::TestCase
|
||||
assert_equal false, fzf.black
|
||||
assert_equal 1, fzf.rxflag
|
||||
assert_equal 'world', fzf.query.get
|
||||
assert_equal false, fzf.select1
|
||||
assert_equal false, fzf.exit0
|
||||
assert_equal 'world', fzf.filter
|
||||
assert_equal nil, fzf.extended
|
||||
assert_equal [4, 5], fzf.nth
|
||||
|
||||
fzf = FZF.new %w[--query hello +s -s 2000 --query=world]
|
||||
assert_equal 2000, fzf.sort
|
||||
assert_equal 'world', fzf.query.get
|
||||
rescue SystemExit => e
|
||||
assert false, "Exited"
|
||||
end
|
||||
@ -538,5 +562,60 @@ class TestFZF < MiniTest::Unit::TestCase
|
||||
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2], regex
|
||||
assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
|
||||
end
|
||||
|
||||
def stream_for str
|
||||
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? }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_select_1
|
||||
stream = stream_for "Hello\nWorld"
|
||||
output = StringIO.new
|
||||
|
||||
begin
|
||||
$stdout = output
|
||||
FZF.new(%w[--query=ol --select-1], stream).start
|
||||
rescue SystemExit => e
|
||||
assert_equal 0, e.status
|
||||
assert_equal 'World', output.string.chomp
|
||||
ensure
|
||||
$stdout = STDOUT
|
||||
end
|
||||
end
|
||||
|
||||
def test_select_1_ambiguity
|
||||
stream = stream_for "Hello\nWorld"
|
||||
begin
|
||||
Timeout::timeout(3) do
|
||||
FZF.new(%w[--query=o --select-1], stream).start
|
||||
end
|
||||
flunk 'Should not reach here'
|
||||
rescue Exception => e
|
||||
Curses.close_screen
|
||||
assert_instance_of Timeout::Error, e
|
||||
end
|
||||
end
|
||||
|
||||
def test_exit_0
|
||||
stream = stream_for "Hello\nWorld"
|
||||
output = StringIO.new
|
||||
|
||||
begin
|
||||
$stdout = output
|
||||
FZF.new(%w[--query=zz --exit-0], stream).start
|
||||
rescue SystemExit => e
|
||||
assert_equal 1, e.status
|
||||
assert_equal '', output.string
|
||||
ensure
|
||||
$stdout = STDOUT
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user