Compare commits

..

10 Commits
0.9.1 ... 0.9.2

Author SHA1 Message Date
Junegunn Choi
9cb0cdb4ac 0.9.2 2015-01-24 14:49:21 +09:00
Junegunn Choi
448132c46c Fix error when --query contains wide-length characters 2015-01-24 13:26:33 +09:00
Junegunn Choi
1476fc7f3b Refactor test code 2015-01-24 13:25:11 +09:00
Junegunn Choi
71a7b3a26f Improve rendering performance by caching rune widths
Related: 8bead4a
2015-01-24 12:28:00 +09:00
Junegunn Choi
a47c06cb61 Fix update_assets script 2015-01-23 20:32:56 +09:00
Junegunn Choi
48e16edb47 Redraw and adjust upon terminal resize 2015-01-23 20:30:50 +09:00
Junegunn Choi
c35d98dc42 Nullify --nth option when it's irrelevant 2015-01-23 06:26:00 +09:00
Junegunn Choi
8bead4ae34 Improved handling of tab characters 2015-01-18 16:59:04 +09:00
Junegunn Choi
1b6cb3532d Update src/README.md 2015-01-18 16:34:10 +09:00
Junegunn Choi
0a0955755a Add note on installation 2015-01-18 16:32:37 +09:00
10 changed files with 209 additions and 80 deletions

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
version=0.9.1
version=0.9.2
cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd)

View File

@@ -19,6 +19,9 @@ git pull
./install
```
Otherwise, follow [the instruction][install] as before. You can also install
fzf using Homebrew if you prefer that way.
Motivations
-----------
@@ -110,6 +113,7 @@ License
[MIT](LICENSE)
[install]: https://github.com/junegunn/fzf#installation
[go]: https://golang.org/
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
[ncurses]: https://www.gnu.org/software/ncurses/

View File

@@ -5,7 +5,7 @@ import (
)
// Current version
const Version = "0.9.1"
const Version = "0.9.2"
// fzf events
const (

View File

@@ -421,6 +421,10 @@ func Clear() {
C.clear()
}
func Endwin() {
C.endwin()
}
func Refresh() {
C.refresh()
}

View File

@@ -266,6 +266,17 @@ func parseOptions(opts *Options, allArgs []string) {
}
}
}
// If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range
if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 {
for _, r := range opts.Nth {
if r.begin == rangeEllipsis && r.end == rangeEllipsis {
opts.Nth = make([]Range, 0)
return
}
}
}
}
// ParseOptions parses command-line options

View File

@@ -21,17 +21,47 @@ func TestSplitNth(t *testing.T) {
}
}
{
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2")
if len(ranges) != 8 ||
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
if len(ranges) != 10 ||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
ranges[1].begin != 1 || ranges[1].end != rangeEllipsis ||
ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
ranges[2].begin != 2 || ranges[2].end != 3 ||
ranges[3].begin != 4 || ranges[3].end != -1 ||
ranges[3].begin != 4 || ranges[3].end != rangeEllipsis ||
ranges[4].begin != -3 || ranges[4].end != -2 ||
ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis ||
ranges[6].begin != 2 || ranges[6].end != 2 ||
ranges[7].begin != -2 || ranges[7].end != -2 {
ranges[7].begin != -2 || ranges[7].end != -2 ||
ranges[8].begin != 2 || ranges[8].end != -2 ||
ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis {
t.Errorf("%s", ranges)
}
}
}
func TestIrrelevantNth(t *testing.T) {
{
opts := defaultOptions()
words := []string{"--nth", "..", "-x"}
parseOptions(opts, words)
if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth)
}
}
for _, words := range [][]string{[]string{"--nth", "..,3"}, []string{"--nth", "3,1.."}, []string{"--nth", "..-1,1"}} {
{
opts := defaultOptions()
parseOptions(opts, words)
if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth)
}
}
{
opts := defaultOptions()
words = append(words, "-x")
parseOptions(opts, words)
if len(opts.Nth) != 2 {
t.Errorf("nth should not be empty: %s", opts.Nth)
}
}
}
}

View File

@@ -1,11 +1,15 @@
package fzf
import (
"bytes"
"fmt"
"os"
"os/signal"
"regexp"
"sort"
"strings"
"sync"
"syscall"
"time"
C "github.com/junegunn/fzf/src/curses"
@@ -58,6 +62,7 @@ func (a ByTimeOrder) Less(i, j int) bool {
}
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
var _runeWidths = make(map[rune]int)
const (
reqPrompt util.EventType = iota
@@ -81,7 +86,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
prompt: opts.Prompt,
tac: opts.Sort == 0,
reverse: opts.Reverse,
cx: displayWidth(input),
cx: len(input),
cy: 0,
offset: 0,
yanked: []rune{},
@@ -169,10 +174,22 @@ func (t *Terminal) output() {
}
}
func runeWidth(r rune, prefixWidth int) int {
if r == '\t' {
return 8 - prefixWidth%8
} else if w, found := _runeWidths[r]; found {
return w
} else {
w := runewidth.RuneWidth(r)
_runeWidths[r] = w
return w
}
}
func displayWidth(runes []rune) int {
l := 0
for _, r := range runes {
l += runewidth.RuneWidth(r)
l += runeWidth(r, l)
}
return l
}
@@ -254,16 +271,27 @@ func (t *Terminal) printItem(item *Item, current bool) {
}
func trimRight(runes []rune, width int) ([]rune, int) {
currentWidth := displayWidth(runes)
trimmed := 0
for currentWidth > width && len(runes) > 0 {
sz := len(runes)
currentWidth -= runewidth.RuneWidth(runes[sz-1])
runes = runes[:sz-1]
trimmed++
// We start from the beginning to handle tab characters
l := 0
for idx, r := range runes {
l += runeWidth(r, l)
if idx > 0 && l > width {
return runes[:idx], len(runes) - idx
}
}
return runes, trimmed
return runes, 0
}
func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
l := 0
for _, r := range runes {
l += runeWidth(r, l+prefixWidth)
if l > limit {
// Early exit
return l
}
}
return l
}
func trimLeft(runes []rune, width int) ([]rune, int32) {
@@ -271,9 +299,9 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
var trimmed int32
for currentWidth > width && len(runes) > 0 {
currentWidth -= runewidth.RuneWidth(runes[0])
runes = runes[1:]
trimmed++
currentWidth = displayWidthWithLimit(runes, 2, width)
}
return runes, trimmed
}
@@ -323,18 +351,41 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
sort.Sort(ByOrder(offsets))
var index int32
var substr string
var prefixWidth int
for _, offset := range offsets {
b := util.Max32(index, offset[0])
e := util.Max32(index, offset[1])
C.CPrint(col1, bold, string(text[index:b]))
C.CPrint(col2, bold, string(text[b:e]))
substr, prefixWidth = processTabs(text[index:b], prefixWidth)
C.CPrint(col1, bold, substr)
substr, prefixWidth = processTabs(text[b:e], prefixWidth)
C.CPrint(col2, bold, substr)
index = e
}
if index < int32(len(text)) {
C.CPrint(col1, bold, string(text[index:]))
substr, _ = processTabs(text[index:], prefixWidth)
C.CPrint(col1, bold, substr)
}
}
func processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf bytes.Buffer
l := prefixWidth
for _, r := range runes {
w := runeWidth(r, l)
l += w
if r == '\t' {
strbuf.WriteString(strings.Repeat(" ", w))
} else {
strbuf.WriteRune(r)
}
}
return strbuf.String(), l
}
func (t *Terminal) printAll() {
t.printList()
t.printInfo()
@@ -408,6 +459,15 @@ func (t *Terminal) Loop() {
<-timer.C
t.reqBox.Set(reqRefresh, nil)
}()
resizeChan := make(chan os.Signal, 1)
signal.Notify(resizeChan, syscall.SIGWINCH)
go func() {
for {
<-resizeChan
t.reqBox.Set(reqRedraw, nil)
}
}()
}
go func() {
@@ -427,6 +487,8 @@ func (t *Terminal) Loop() {
t.suppress = false
case reqRedraw:
C.Clear()
C.Endwin()
C.Refresh()
t.printAll()
case reqClose:
C.Close()

View File

@@ -28,22 +28,32 @@ type Token struct {
prefixLength int
}
func newRange(begin int, end int) Range {
if begin == 1 {
begin = rangeEllipsis
}
if end == -1 {
end = rangeEllipsis
}
return Range{begin, end}
}
// ParseRange parses nth-expression and returns the corresponding Range object
func ParseRange(str *string) (Range, bool) {
if (*str) == ".." {
return Range{rangeEllipsis, rangeEllipsis}, true
return newRange(rangeEllipsis, rangeEllipsis), true
} else if strings.HasPrefix(*str, "..") {
end, err := strconv.Atoi((*str)[2:])
if err != nil || end == 0 {
return Range{}, false
}
return Range{rangeEllipsis, end}, true
return newRange(rangeEllipsis, end), true
} else if strings.HasSuffix(*str, "..") {
begin, err := strconv.Atoi((*str)[:len(*str)-2])
if err != nil || begin == 0 {
return Range{}, false
}
return Range{begin, rangeEllipsis}, true
return newRange(begin, rangeEllipsis), true
} else if strings.Contains(*str, "..") {
ns := strings.Split(*str, "..")
if len(ns) != 2 {
@@ -51,17 +61,17 @@ func ParseRange(str *string) (Range, bool) {
}
begin, err1 := strconv.Atoi(ns[0])
end, err2 := strconv.Atoi(ns[1])
if err1 != nil || err2 != nil {
if err1 != nil || err2 != nil || begin == 0 || end == 0 {
return Range{}, false
}
return Range{begin, end}, true
return newRange(begin, end), true
}
n, err := strconv.Atoi(*str)
if err != nil || n == 0 {
return Range{}, false
}
return Range{n, n}, true
return newRange(n, n), true
}
func withPrefixLengths(tokens []string, begin int) []Token {

View File

@@ -5,6 +5,7 @@ require 'rest_client'
if ARGV.length < 3
puts "usage: #$0 <token> <version> <files...>"
exit 1
end
token, version, *files = ARGV

View File

@@ -9,13 +9,36 @@ class NilClass
end
end
module Temp
def readonce
name = self.class::TEMPNAME
waited = 0
while waited < 5
begin
data = File.read(name)
return data unless data.empty?
rescue
sleep 0.1
waited += 0.1
end
end
raise "failed to read tempfile"
ensure
while File.exists? name
File.unlink name rescue nil
end
end
end
class Tmux
include Temp
TEMPNAME = '/tmp/fzf-test.txt'
attr_reader :win
def initialize shell = 'bash'
@win = go("new-window -d -P -F '#I' 'PS1= bash --rcfile ~/.fzf.#{shell}'").first
@win = go("new-window -d -P -F '#I' 'PS1=FIN PROMPT_COMMAND= bash --rcfile ~/.fzf.#{shell}'").first
@lines = `tput lines`.chomp.to_i
end
@@ -40,7 +63,7 @@ class Tmux
def capture
go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}")
raise "Window not found" if $?.exitstatus != 0
File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end
def until timeout = 1
@@ -71,36 +94,16 @@ private
end
class TestGoFZF < MiniTest::Unit::TestCase
include Temp
TEMPNAME = '/tmp/output'
attr_reader :tmux
def tempname
'/tmp/output'
end
def rmtemp
while File.exists? tempname
File.unlink tempname rescue nil
end
end
def readtemp
waited = 0
while waited < 5
begin
return File.read(tempname)
rescue
sleep 0.1
waited += 0.1
end
end
raise "failed to read tempfile"
end
def setup
ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND'
@tmux = Tmux.new
rmtemp
end
def teardown
@@ -108,7 +111,7 @@ class TestGoFZF < MiniTest::Unit::TestCase
end
def test_vanilla
tmux.send_keys "seq 1 100000 | fzf > #{tempname}", :Enter
tmux.send_keys "seq 1 100000 | fzf > #{TEMPNAME}", :Enter
tmux.until(10) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
lines = tmux.capture
assert_equal ' 2', lines[-4]
@@ -127,16 +130,16 @@ class TestGoFZF < MiniTest::Unit::TestCase
tmux.send_keys :Enter
tmux.close
assert_equal '1391', readtemp.chomp
assert_equal '1391', readonce.chomp
end
def test_fzf_default_command
tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' fzf > #{tempname}", :Enter
tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' fzf > #{TEMPNAME}", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Enter
tmux.close
assert_equal 'hello', readtemp.chomp
assert_equal 'hello', readonce.chomp
end
def test_key_bindings
@@ -206,7 +209,7 @@ class TestGoFZF < MiniTest::Unit::TestCase
end
def test_multi_order
tmux.send_keys "seq 1 10 | fzf --multi > #{tempname} && echo -n done", :Enter
tmux.send_keys "seq 1 10 | fzf --multi > #{TEMPNAME}", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Tab, :Up, :Up, :Tab, :Tab, :Tab, # 3, 2
@@ -214,18 +217,16 @@ class TestGoFZF < MiniTest::Unit::TestCase
:PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7
tmux.until { |lines| lines[-2].include? '(6)' }
tmux.send_keys "C-M"
tmux.until { |lines| lines[-1].include?('done') }
assert_equal %w[3 2 5 6 8 7], readtemp.split($/)
tmux.until { |lines| lines[-1].include?('FIN') }
assert_equal %w[3 2 5 6 8 7], readonce.split($/)
tmux.close
end
def test_with_nth
[true, false].each do |multi|
rmtemp
tmux.send_keys "(echo ' 1st 2nd 3rd/';
echo ' first second third/') |
fzf #{"--multi" if multi} -x --nth 2 --with-nth 2,-1,1 > #{tempname} && echo -n done",
fzf #{"--multi" if multi} -x --nth 2 --with-nth 2,-1,1 > #{TEMPNAME}",
:Enter
tmux.until { |lines| lines[-2].include?('2/2') }
@@ -237,42 +238,48 @@ class TestGoFZF < MiniTest::Unit::TestCase
# However, the output must not be transformed
if multi
tmux.send_keys :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1].include?('done') }
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readtemp.split($/)
tmux.until { |lines| lines[-1].include?('FIN') }
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/)
else
tmux.send_keys '^', '3'
tmux.until { |lines| lines[-2].include?('1/2') }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?('done') }
assert_equal [' 1st 2nd 3rd/'], readtemp.split($/)
tmux.until { |lines| lines[-1].include?('FIN') }
assert_equal [' 1st 2nd 3rd/'], readonce.split($/)
end
end
end
def test_scroll
[true, false].each do |rev|
rmtemp
tmux.send_keys "seq 1 100 | fzf #{'--reverse' if rev} > #{tempname} && echo -n done", :Enter
tmux.until { |lines| rev ? lines.first == '>' : lines.last == '>' }
tmux.send_keys "seq 1 100 | fzf #{'--reverse' if rev} > #{TEMPNAME}", :Enter
tmux.until { |lines| lines.include? ' 100/100' }
tmux.send_keys *110.times.map { rev ? :Down : :Up }
tmux.until { |lines| lines.include? '> 100' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?('done') }
assert_equal '100', readtemp.chomp
tmux.until { |lines| lines[-1].include?('FIN') }
assert_equal '100', readonce.chomp
end
end
def test_select_1
tmux.send_keys "seq 1 100 | fzf --with-nth ..,.. --print-query -q 5555 -1 > #{tempname} && echo -n done", :Enter
tmux.until { |lines| lines[-1].include?('done') }
assert_equal ['5555', '55'], readtemp.split($/)
tmux.send_keys "seq 1 100 | fzf --with-nth ..,.. --print-query -q 5555 -1 > #{TEMPNAME}", :Enter
tmux.until { |lines| lines[-1].include?('FIN') }
assert_equal ['5555', '55'], readonce.split($/)
end
def test_exit_0
tmux.send_keys "seq 1 100 | fzf --with-nth ..,.. --print-query -q 555555 -0 > #{tempname} && echo -n done", :Enter
tmux.until { |lines| lines[-1].include?('done') }
assert_equal ['555555'], readtemp.split($/)
tmux.send_keys "seq 1 100 | fzf --with-nth ..,.. --print-query -q 555555 -0 > #{TEMPNAME}", :Enter
tmux.until { |lines| lines[-1].include?('FIN') }
assert_equal ['555555'], readonce.split($/)
end
def test_query_unicode
tmux.send_keys "(echo abc; echo 가나다) | fzf --query 가다 > #{TEMPNAME}", :Enter
tmux.until { |lines| lines.last.start_with? '>' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?('FIN') }
assert_equal ['가나다'], readonce.split($/)
end
end