summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2014-03-29 17:05:35 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2014-03-30 15:12:04 +0900
commitec4b8a59fad7247922444b80525215605fd9b9dc (patch)
tree48b6e218e81e1442d4f2cf8515b24a132ff72776
parentcf8dbf804769fd41bea6982a9c17dda3b74fb106 (diff)
Implement --nth and --delimiter option
-rw-r--r--README.md10
-rwxr-xr-xfzf84
-rw-r--r--test/test_fzf.rb64
3 files changed, 144 insertions, 14 deletions
diff --git a/README.md b/README.md
index f0bd85df..624399cd 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 5c3d50ff..a623e00e 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 b236d6ea..c9ac9047 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