From f3177305d5572b26f135fc045481358b4eb1bf69 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 2 Jan 2015 04:49:30 +0900 Subject: [PATCH 001/408] Rewrite fzf in Go --- .gitignore | 2 + install | 126 +++++---- src/Dockerfile | 33 +++ src/LICENSE | 21 ++ src/Makefile | 49 ++++ src/README.md | 59 +++++ src/algo.go | 152 +++++++++++ src/algo_test.go | 44 ++++ src/atomicbool.go | 27 ++ src/atomicbool_test.go | 17 ++ src/cache.go | 47 ++++ src/chunklist.go | 73 ++++++ src/chunklist_test.go | 66 +++++ src/constants.go | 12 + src/core.go | 153 +++++++++++ src/curses/curses.go | 424 ++++++++++++++++++++++++++++++ src/eventbox.go | 48 ++++ src/fzf/main.go | 7 + src/item.go | 135 ++++++++++ src/item_test.go | 78 ++++++ src/matcher.go | 215 +++++++++++++++ src/options.go | 276 ++++++++++++++++++++ src/options_test.go | 37 +++ src/pattern.go | 305 ++++++++++++++++++++++ src/pattern_test.go | 87 +++++++ src/reader.go | 60 +++++ src/reader_test.go | 52 ++++ src/terminal.go | 580 +++++++++++++++++++++++++++++++++++++++++ src/tokenizer.go | 194 ++++++++++++++ src/tokenizer_test.go | 97 +++++++ src/util.go | 21 ++ src/util_test.go | 18 ++ 32 files changed, 3466 insertions(+), 49 deletions(-) create mode 100644 src/Dockerfile create mode 100644 src/LICENSE create mode 100644 src/Makefile create mode 100644 src/README.md create mode 100644 src/algo.go create mode 100644 src/algo_test.go create mode 100644 src/atomicbool.go create mode 100644 src/atomicbool_test.go create mode 100644 src/cache.go create mode 100644 src/chunklist.go create mode 100644 src/chunklist_test.go create mode 100644 src/constants.go create mode 100644 src/core.go create mode 100644 src/curses/curses.go create mode 100644 src/eventbox.go create mode 100644 src/fzf/main.go create mode 100644 src/item.go create mode 100644 src/item_test.go create mode 100644 src/matcher.go create mode 100644 src/options.go create mode 100644 src/options_test.go create mode 100644 src/pattern.go create mode 100644 src/pattern_test.go create mode 100644 src/reader.go create mode 100644 src/reader_test.go create mode 100644 src/terminal.go create mode 100644 src/tokenizer.go create mode 100644 src/tokenizer_test.go create mode 100644 src/util.go create mode 100644 src/util_test.go diff --git a/.gitignore b/.gitignore index 1627430..0915467 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +bin +src/fzf/fzf_* pkg Gemfile.lock .DS_Store diff --git a/install b/install index 3176b27..6b64a3f 100755 --- a/install +++ b/install @@ -3,60 +3,81 @@ cd `dirname $BASH_SOURCE` fzf_base=`pwd` -# ruby executable -echo -n "Checking Ruby executable ... " -ruby=`which ruby` -if [ $? -ne 0 ]; then - echo "ruby executable not found!" - exit 1 -fi +ARCHI=$(uname -sm) -# System ruby is preferred -system_ruby=/usr/bin/ruby -if [ -x $system_ruby -a $system_ruby != "$ruby" ]; then - $system_ruby --disable-gems -rcurses -e0 2> /dev/null - [ $? -eq 0 ] && ruby=$system_ruby -fi - -echo "OK ($ruby)" - -# Curses-support -echo -n "Checking Curses support ... " -"$ruby" -rcurses -e0 2> /dev/null -if [ $? -eq 0 ]; then - echo "OK" -else - echo "Not found" - echo "Installing 'curses' gem ... " - if (( EUID )); then - /usr/bin/env gem install curses --user-install +download() { + echo "Downloading fzf executable ($1) ..." + if curl -fLo "$fzf_base"/bin/fzf https://github.com/junegunn/fzf-bin/releases/download/snapshot/$1; then + chmod +x "$fzf_base"/bin/fzf else - /usr/bin/env gem install curses - fi - if [ $? -ne 0 ]; then - echo - echo "Failed to install 'curses' gem." - if [[ $(uname -r) =~ 'ARCH' ]]; then - echo "Make sure that base-devel package group is installed." - fi + echo "Failed to download $1" exit 1 fi -fi +} -# Ruby version -echo -n "Checking Ruby version ... " -"$ruby" -e 'exit RUBY_VERSION >= "1.9"' -if [ $? -eq 0 ]; then - echo ">= 1.9" - "$ruby" --disable-gems -rcurses -e0 2> /dev/null +mkdir -p "$fzf_base"/bin +if [ "$ARCHI" = "Darwin x86_64" ]; then + download fzf_darwin_amd64 +elif [ "$ARCHI" = "Linux x86_64" ]; then + download fzf_linux_amd64 +else # No prebuilt executable + echo "No prebuilt binary for $ARCHI ... Installing legacy Ruby version ..." + + # ruby executable + echo -n "Checking Ruby executable ... " + ruby=`which ruby` + if [ $? -ne 0 ]; then + echo "ruby executable not found!" + exit 1 + fi + + # System ruby is preferred + system_ruby=/usr/bin/ruby + if [ -x $system_ruby -a $system_ruby != "$ruby" ]; then + $system_ruby --disable-gems -rcurses -e0 2> /dev/null + [ $? -eq 0 ] && ruby=$system_ruby + fi + + echo "OK ($ruby)" + + # Curses-support + echo -n "Checking Curses support ... " + "$ruby" -rcurses -e0 2> /dev/null if [ $? -eq 0 ]; then - fzf_cmd="$ruby --disable-gems $fzf_base/fzf" + echo "OK" else + echo "Not found" + echo "Installing 'curses' gem ... " + if (( EUID )); then + /usr/bin/env gem install curses --user-install + else + /usr/bin/env gem install curses + fi + if [ $? -ne 0 ]; then + echo + echo "Failed to install 'curses' gem." + if [[ $(uname -r) =~ 'ARCH' ]]; then + echo "Make sure that base-devel package group is installed." + fi + exit 1 + fi + fi + + # Ruby version + echo -n "Checking Ruby version ... " + "$ruby" -e 'exit RUBY_VERSION >= "1.9"' + if [ $? -eq 0 ]; then + echo ">= 1.9" + "$ruby" --disable-gems -rcurses -e0 2> /dev/null + if [ $? -eq 0 ]; then + fzf_cmd="$ruby --disable-gems $fzf_base/fzf" + else + fzf_cmd="$ruby $fzf_base/fzf" + fi + else + echo "< 1.9" fzf_cmd="$ruby $fzf_base/fzf" fi -else - echo "< 1.9" - fzf_cmd="$ruby $fzf_base/fzf" fi # Auto-completion @@ -85,10 +106,17 @@ for shell in bash zsh; do # Setup fzf function # ------------------ unalias fzf 2> /dev/null -fzf() { - $fzf_cmd "\$@" -} -export -f fzf > /dev/null +unset fzf 2> /dev/null +if [ -x "$fzf_base/bin/fzf" ]; then + if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then + export PATH="$fzf_base/bin:\$PATH" + fi +else + fzf() { + $fzf_cmd "\$@" + } + export -f fzf > /dev/null +fi # Auto-completion # --------------- diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..3c062ee --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,33 @@ +FROM ubuntu:14.04 +MAINTAINER Junegunn Choi + +# apt-get +RUN apt-get update && apt-get -y upgrade +RUN apt-get install -y --force-yes git vim-nox curl procps sudo \ + build-essential libncurses-dev + +# Setup jg user with sudo privilege +RUN useradd -s /bin/bash -m jg && echo 'jg:jg' | chpasswd && \ + echo 'jg ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/jg + +# Setup dotfiles +USER jg +RUN cd ~ && git clone https://github.com/junegunn/dotfiles.git && \ + dotfiles/install > /dev/null + +# Install Go 1.4 +RUN cd ~ && curl https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | tar -xz && \ + mv go go1.4 && \ + echo 'export GOROOT=~/go1.4' >> ~/dotfiles/bashrc-extra && \ + echo 'export PATH=~/go1.4/bin:$PATH' >> ~/dotfiles/bashrc-extra + +# Symlink fzf directory +RUN mkdir -p ~jg/go/src/github.com/junegunn && \ + ln -s /fzf ~jg/go/src/github.com/junegunn/fzf + +# Volume +VOLUME /fzf + +# Default CMD +CMD cd ~jg/go/src/github.com/junegunn/fzf/src && /bin/bash -l + diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..fe4c31a --- /dev/null +++ b/src/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..bae4c90 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,49 @@ +BINARY := fzf/fzf + +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + BINARY := $(BINARY)_darwin +else ifeq ($(UNAME_S),Linux) + BINARY := $(BINARY)_linux +endif + +UNAME_M := $(shell uname -m) +ifneq ($(filter i386 i686,$(UNAME_M)),) +$(error "filtered is not supported, yet.") +endif + +ifeq ($(UNAME_M),x86_64) + BINARY := $(BINARY)_amd64 +else ifneq ($(filter i386 i686,$(UNAME_M)),) + BINARY := $(BINARY)_386 +else # TODO +$(error "$(UNAME_M) is not supported, yet.") +endif + +BINDIR = ../bin +SOURCES = $(wildcard *.go fzf/*.go) + +all: build + +build: $(BINARY) + +$(BINARY): $(SOURCES) + go get + go test -v + cd fzf && go build -o $(notdir $(BINARY)) + +install: $(BINARY) + mkdir -p $(BINDIR) + cp -f $(BINARY) $(BINDIR)/fzf + +clean: + rm -f $(BINARY) + +docker: + docker build -t junegunn/ubuntu-sandbox . + +linux64: + docker run -i -t -u jg -v $(shell cd ..; pwd):/fzf junegunn/ubuntu-sandbox \ + /bin/bash -ci 'cd ~jg/go/src/github.com/junegunn/fzf/src; make build' + +.PHONY: build install linux64 clean docker run diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..2f3ca3b --- /dev/null +++ b/src/README.md @@ -0,0 +1,59 @@ +fzf in Go +========= + +This directory contains the source code for the new fzf implementation in Go. +This new version has the following benefits over the previous Ruby version. + +- Immensely faster + - No GIL. Performance is linearly proportional to the number of cores. + - It's so fast that I even decided to remove the sort limit (`--sort=N`) +- Does not require Ruby and distributed as an executable binary + - Ruby dependency is especially painful on Ruby 2.1 or above which + ships without curses gem + +Build +----- + +```sh +# Build fzf executable +make + +# Install the executable to ../bin directory +make install + +# Build executable for Linux x86_64 using Docker +make linux64 +``` + + +Prebuilt binaries +----------------- + +- Darwin x86_64 +- Linux x86_64 + +Third-party libraries used +-------------------------- + +- [ncurses](https://www.gnu.org/software/ncurses/) +- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) + - Licensed under [MIT](http://mattn.mit-license.org/2013) +- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) + - Licensed under [MIT](http://mattn.mit-license.org/2014) + +Contribution +------------ + +For the moment, I will not add or accept any new features until we can be sure +that the implementation is stable and we have a sufficient number of test +cases. However, fixes for obvious bugs and new test cases are welcome. + +I also care much about the performance of the implementation (that's the +reason I rewrote the whole thing in Go, right?), so please make sure that your +change does not result in performance regression. Please be minded that we +still don't have a quantitative measure of the performance. + +License +------- + +- [MIT](LICENSE) diff --git a/src/algo.go b/src/algo.go new file mode 100644 index 0000000..16790ba --- /dev/null +++ b/src/algo.go @@ -0,0 +1,152 @@ +package fzf + +import "strings" + +/* + * String matching algorithms here do not use strings.ToLower to avoid + * performance penalty. And they assume pattern runes are given in lowercase + * letters when caseSensitive is false. + * + * In short: They try to do as little work as possible. + */ + +func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(*input) + + // 0. (FIXME) How to find the shortest match? + // a_____b__c__abc + // ^^^^^^^^^^ ^^^ + // 1. forward scan (abc) + // *-----*-----*> + // a_____b___abc__ + // 2. reverse scan (cba) + // a_____b___abc__ + // <*** + pidx := 0 + sidx := -1 + eidx := -1 + + for index, char := range runes { + // This is considerably faster than blindly applying strings.ToLower to the + // whole string + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if char == pattern[pidx] { + if sidx < 0 { + sidx = index + } + if pidx += 1; pidx == len(pattern) { + eidx = index + 1 + break + } + } + } + + if sidx >= 0 && eidx >= 0 { + pidx -= 1 + for index := eidx - 1; index >= sidx; index-- { + char := runes[index] + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if char == pattern[pidx] { + if pidx -= 1; pidx < 0 { + sidx = index + break + } + } + } + return sidx, eidx + } + return -1, -1 +} + +func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, int) { + var str string + if caseSensitive { + str = *input + } else { + str = strings.ToLower(*input) + } + + if idx := strings.Index(str, string(pattern)); idx >= 0 { + prefixRuneLen := len([]rune((*input)[:idx])) + return prefixRuneLen, prefixRuneLen + len(pattern) + } + return -1, -1 +} + +/* + * This is a basic string searching algorithm that handles case sensitivity. + * Although naive, it still performs better than the combination of + * strings.ToLower + strings.Index for typical fzf use cases where input + * strings and patterns are not very long. + * + * We might try to implement better algorithms in the future: + * http://en.wikipedia.org/wiki/String_searching_algorithm + */ +func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(*input) + numRunes := len(runes) + plen := len(pattern) + if len(runes) < plen { + return -1, -1 + } + + pidx := 0 + for index := 0; index < numRunes; index++ { + char := runes[index] + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if pattern[pidx] == char { + pidx += 1 + if pidx == plen { + return index - plen + 1, index + 1 + } + } else { + index -= pidx + pidx = 0 + } + } + return -1, -1 +} + +func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(*input) + if len(runes) < len(pattern) { + return -1, -1 + } + + for index, r := range pattern { + char := runes[index] + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if char != r { + return -1, -1 + } + } + return 0, len(pattern) +} + +func SuffixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { + runes := []rune(strings.TrimRight(*input, " ")) + trimmedLen := len(runes) + diff := trimmedLen - len(pattern) + if diff < 0 { + return -1, -1 + } + + for index, r := range pattern { + char := runes[index+diff] + if !caseSensitive && char >= 65 && char <= 90 { + char += 32 + } + if char != r { + return -1, -1 + } + } + return trimmedLen - len(pattern), trimmedLen +} diff --git a/src/algo_test.go b/src/algo_test.go new file mode 100644 index 0000000..5da01a6 --- /dev/null +++ b/src/algo_test.go @@ -0,0 +1,44 @@ +package fzf + +import ( + "strings" + "testing" +) + +func assertMatch(t *testing.T, fun func(bool, *string, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { + if !caseSensitive { + pattern = strings.ToLower(pattern) + } + s, e := fun(caseSensitive, &input, []rune(pattern)) + if s != sidx { + t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) + } + if e != eidx { + t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", e, eidx, input, pattern) + } +} + +func TestFuzzyMatch(t *testing.T) { + assertMatch(t, FuzzyMatch, false, "fooBarbaz", "oBZ", 2, 9) + assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBZ", -1, -1) + assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBz", 2, 9) + assertMatch(t, FuzzyMatch, true, "fooBarbaz", "fooBarbazz", -1, -1) +} + +func TestExactMatchNaive(t *testing.T) { + assertMatch(t, ExactMatchNaive, false, "fooBarbaz", "oBA", 2, 5) + assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "oBA", -1, -1) + assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "fooBarbazz", -1, -1) +} + +func TestPrefixMatch(t *testing.T) { + assertMatch(t, PrefixMatch, false, "fooBarbaz", "Foo", 0, 3) + assertMatch(t, PrefixMatch, true, "fooBarbaz", "Foo", -1, -1) + assertMatch(t, PrefixMatch, false, "fooBarbaz", "baz", -1, -1) +} + +func TestSuffixMatch(t *testing.T) { + assertMatch(t, SuffixMatch, false, "fooBarbaz", "Foo", -1, -1) + assertMatch(t, SuffixMatch, false, "fooBarbaz", "baz", 6, 9) + assertMatch(t, SuffixMatch, true, "fooBarbaz", "Baz", -1, -1) +} diff --git a/src/atomicbool.go b/src/atomicbool.go new file mode 100644 index 0000000..f2f4894 --- /dev/null +++ b/src/atomicbool.go @@ -0,0 +1,27 @@ +package fzf + +import "sync" + +type AtomicBool struct { + mutex sync.Mutex + state bool +} + +func NewAtomicBool(initialState bool) *AtomicBool { + return &AtomicBool{ + mutex: sync.Mutex{}, + state: initialState} +} + +func (a *AtomicBool) Get() bool { + a.mutex.Lock() + defer a.mutex.Unlock() + return a.state +} + +func (a *AtomicBool) Set(newState bool) bool { + a.mutex.Lock() + defer a.mutex.Unlock() + a.state = newState + return a.state +} diff --git a/src/atomicbool_test.go b/src/atomicbool_test.go new file mode 100644 index 0000000..0af4570 --- /dev/null +++ b/src/atomicbool_test.go @@ -0,0 +1,17 @@ +package fzf + +import "testing" + +func TestAtomicBool(t *testing.T) { + if !NewAtomicBool(true).Get() || NewAtomicBool(false).Get() { + t.Error("Invalid initial value") + } + + ab := NewAtomicBool(true) + if ab.Set(false) { + t.Error("Invalid return value") + } + if ab.Get() { + t.Error("Invalid state") + } +} diff --git a/src/cache.go b/src/cache.go new file mode 100644 index 0000000..340f325 --- /dev/null +++ b/src/cache.go @@ -0,0 +1,47 @@ +package fzf + +import "sync" + +type QueryCache map[string][]*Item +type ChunkCache struct { + mutex sync.Mutex + cache map[*Chunk]*QueryCache +} + +func NewChunkCache() ChunkCache { + return ChunkCache{sync.Mutex{}, make(map[*Chunk]*QueryCache)} +} + +func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { + if len(key) == 0 || !chunk.IsFull() { + return + } + + cc.mutex.Lock() + defer cc.mutex.Unlock() + + qc, ok := cc.cache[chunk] + if !ok { + cc.cache[chunk] = &QueryCache{} + qc = cc.cache[chunk] + } + (*qc)[key] = list +} + +func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) { + if len(key) == 0 || !chunk.IsFull() { + return nil, false + } + + cc.mutex.Lock() + defer cc.mutex.Unlock() + + qc, ok := cc.cache[chunk] + if ok { + list, ok := (*qc)[key] + if ok { + return list, true + } + } + return nil, false +} diff --git a/src/chunklist.go b/src/chunklist.go new file mode 100644 index 0000000..b1f9638 --- /dev/null +++ b/src/chunklist.go @@ -0,0 +1,73 @@ +package fzf + +import "sync" + +const CHUNK_SIZE int = 100 + +type Chunk []*Item // >>> []Item + +type Transformer func(*string, int) *Item + +type ChunkList struct { + chunks []*Chunk + count int + mutex sync.Mutex + trans Transformer +} + +func NewChunkList(trans Transformer) *ChunkList { + return &ChunkList{ + chunks: []*Chunk{}, + count: 0, + mutex: sync.Mutex{}, + trans: trans} +} + +func (c *Chunk) push(trans Transformer, data *string, index int) { + *c = append(*c, trans(data, index)) +} + +func (c *Chunk) IsFull() bool { + return len(*c) == CHUNK_SIZE +} + +func (cl *ChunkList) lastChunk() *Chunk { + return cl.chunks[len(cl.chunks)-1] +} + +func CountItems(cs []*Chunk) int { + if len(cs) == 0 { + return 0 + } + return CHUNK_SIZE*(len(cs)-1) + len(*(cs[len(cs)-1])) +} + +func (cl *ChunkList) Count() int { + return cl.count +} + +func (cl *ChunkList) Chunks() []*Chunk { + return cl.chunks +} + +func (cl *ChunkList) Push(data string) { + cl.mutex.Lock() + defer cl.mutex.Unlock() + + if len(cl.chunks) == 0 || cl.lastChunk().IsFull() { + newChunk := Chunk(make([]*Item, 0, CHUNK_SIZE)) + cl.chunks = append(cl.chunks, &newChunk) + } + + cl.lastChunk().push(cl.trans, &data, cl.count) + cl.count += 1 +} + +func (cl *ChunkList) Snapshot() []*Chunk { + cl.mutex.Lock() + defer cl.mutex.Unlock() + + ret := make([]*Chunk, len(cl.chunks)) + copy(ret, cl.chunks) + return ret +} diff --git a/src/chunklist_test.go b/src/chunklist_test.go new file mode 100644 index 0000000..a7daa47 --- /dev/null +++ b/src/chunklist_test.go @@ -0,0 +1,66 @@ +package fzf + +import ( + "fmt" + "testing" +) + +func TestChunkList(t *testing.T) { + cl := NewChunkList(func(s *string, i int) *Item { + return &Item{text: s, index: i * 2} + }) + + // Snapshot + snapshot := cl.Snapshot() + if len(snapshot) > 0 { + t.Error("Snapshot should be empty now") + } + + // Add some data + cl.Push("hello") + cl.Push("world") + + // Previously created snapshot should remain the same + if len(snapshot) > 0 { + t.Error("Snapshot should not have changed") + } + + // But the new snapshot should contain the added items + snapshot = cl.Snapshot() + if len(snapshot) != 1 { + t.Error("Snapshot should not be empty now") + } + + // Check the content of the ChunkList + chunk1 := snapshot[0] + if len(*chunk1) != 2 { + t.Error("Snapshot should contain only two items") + } + if *(*chunk1)[0].text != "hello" || (*chunk1)[0].index != 0 || + *(*chunk1)[1].text != "world" || (*chunk1)[1].index != 2 { + t.Error("Invalid data") + } + if chunk1.IsFull() { + t.Error("Chunk should not have been marked full yet") + } + + // Add more data + for i := 0; i < CHUNK_SIZE*2; i++ { + cl.Push(fmt.Sprintf("item %d", i)) + } + + // Previous snapshot should remain the same + if len(snapshot) != 1 { + t.Error("Snapshot should stay the same") + } + + // New snapshot + snapshot = cl.Snapshot() + if len(snapshot) != 3 || !snapshot[0].IsFull() || + !snapshot[1].IsFull() || snapshot[2].IsFull() { + t.Error("Expected two full chunks and one more chunk") + } + if len(*snapshot[2]) != 2 { + t.Error("Unexpected number of items") + } +} diff --git a/src/constants.go b/src/constants.go new file mode 100644 index 0000000..b0b64db --- /dev/null +++ b/src/constants.go @@ -0,0 +1,12 @@ +package fzf + +const VERSION = "0.9.0" + +const ( + EVT_READ_NEW EventType = iota + EVT_READ_FIN + EVT_SEARCH_NEW + EVT_SEARCH_PROGRESS + EVT_SEARCH_FIN + EVT_CLOSE +) diff --git a/src/core.go b/src/core.go new file mode 100644 index 0000000..2601397 --- /dev/null +++ b/src/core.go @@ -0,0 +1,153 @@ +package fzf + +import ( + "fmt" + "os" + "runtime" + "time" +) + +const COORDINATOR_DELAY time.Duration = 100 * time.Millisecond + +func initProcs() { + runtime.GOMAXPROCS(runtime.NumCPU()) +} + +/* +Reader -> EVT_READ_FIN +Reader -> EVT_READ_NEW -> Matcher (restart) +Terminal -> EVT_SEARCH_NEW -> Matcher (restart) +Matcher -> EVT_SEARCH_PROGRESS -> Terminal (update info) +Matcher -> EVT_SEARCH_FIN -> Terminal (update list) +*/ + +func Run(options *Options) { + initProcs() + + opts := ParseOptions() + + if opts.Version { + fmt.Println(VERSION) + os.Exit(0) + } + + // Event channel + eventBox := NewEventBox() + + // Chunk list + var chunkList *ChunkList + if len(opts.WithNth) == 0 { + chunkList = NewChunkList(func(data *string, index int) *Item { + return &Item{text: data, index: index} + }) + } else { + chunkList = NewChunkList(func(data *string, index int) *Item { + item := Item{text: data, index: index} + tokens := Tokenize(item.text, opts.Delimiter) + item.origText = item.text + item.text = Transform(tokens, opts.WithNth).whole + return &item + }) + } + + // Reader + reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} + go reader.ReadSource() + + // Matcher + patternBuilder := func(runes []rune) *Pattern { + return BuildPattern( + opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) + } + matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox) + + // Defered-interactive / Non-interactive + // --select-1 | --exit-0 | --filter + if filtering := opts.Filter != nil; filtering || opts.Select1 || opts.Exit0 { + limit := 0 + var patternString string + if filtering { + patternString = *opts.Filter + } else { + if opts.Select1 || opts.Exit0 { + limit = 1 + } + patternString = opts.Query + } + pattern := patternBuilder([]rune(patternString)) + + looping := true + for looping { + eventBox.Wait(func(events *Events) { + for evt, _ := range *events { + switch evt { + case EVT_READ_FIN: + looping = false + return + } + } + }) + time.Sleep(COORDINATOR_DELAY) + } + + matches, cancelled := matcher.scan(MatchRequest{ + chunks: chunkList.Snapshot(), + pattern: pattern}, limit) + + if !cancelled && (filtering || opts.Exit0) { + if opts.PrintQuery { + fmt.Println(patternString) + } + for _, item := range matches { + item.Print() + } + os.Exit(0) + } + } + + // Go interactive + go matcher.Loop() + + // Terminal I/O + terminal := NewTerminal(opts, eventBox) + go terminal.Loop() + + // Event coordination + reading := true + ticks := 0 + for { + delay := true + ticks += 1 + eventBox.Wait(func(events *Events) { + defer events.Clear() + for evt, value := range *events { + switch evt { + + case EVT_READ_NEW, EVT_READ_FIN: + reading = reading && evt == EVT_READ_NEW + terminal.UpdateCount(chunkList.Count(), !reading) + matcher.Reset(chunkList.Snapshot(), terminal.Input(), false) + + case EVT_SEARCH_NEW: + matcher.Reset(chunkList.Snapshot(), terminal.Input(), true) + delay = false + + case EVT_SEARCH_PROGRESS: + switch val := value.(type) { + case float32: + terminal.UpdateProgress(val) + } + + case EVT_SEARCH_FIN: + switch val := value.(type) { + case []*Item: + terminal.UpdateList(val) + } + } + } + }) + if ticks > 3 && delay && reading { + time.Sleep(COORDINATOR_DELAY) + } + } +} diff --git a/src/curses/curses.go b/src/curses/curses.go new file mode 100644 index 0000000..945a3ce --- /dev/null +++ b/src/curses/curses.go @@ -0,0 +1,424 @@ +package curses + +// #include +// #include +// #cgo LDFLAGS: -lncurses +import "C" + +import ( + "os" + "os/signal" + "syscall" + "time" + "unicode/utf8" +) + +const ( + RUNE = iota + + CTRL_A + CTRL_B + CTRL_C + CTRL_D + CTRL_E + CTRL_F + CTRL_G + CTRL_H + TAB + CTRL_J + CTRL_K + CTRL_L + CTRL_M + CTRL_N + CTRL_O + CTRL_P + CTRL_Q + CTRL_R + CTRL_S + CTRL_T + CTRL_U + CTRL_V + CTRL_W + CTRL_X + CTRL_Y + CTRL_Z + ESC + + INVALID + MOUSE + + BTAB + + DEL + PGUP + PGDN + + ALT_B + ALT_F + ALT_D + ALT_BS +) + +const ( + COL_NORMAL = iota + COL_PROMPT + COL_MATCH + COL_CURRENT + COL_CURRENT_MATCH + COL_SPINNER + COL_INFO + COL_CURSOR + COL_SELECTED +) + +const ( + DOUBLE_CLICK_DURATION = 500 * time.Millisecond +) + +type Event struct { + Type int + Char rune + MouseEvent *MouseEvent +} + +type MouseEvent struct { + Y int + X int + S int + Down bool + Double bool + Mod bool +} + +var ( + _buf []byte + _in *os.File + _color func(int, bool) C.int + _prevDownTime time.Time + _prevDownY int + _clickY []int +) + +func init() { + _prevDownTime = time.Unix(0, 0) + _clickY = []int{} +} + +func attrColored(pair int, bold bool) C.int { + var attr C.int = 0 + if pair > COL_NORMAL { + attr = C.COLOR_PAIR(C.int(pair)) + } + if bold { + attr = attr | C.A_BOLD + } + return attr +} + +func attrMono(pair int, bold bool) C.int { + var attr C.int = 0 + switch pair { + case COL_CURRENT: + if bold { + attr = C.A_REVERSE + } + case COL_MATCH: + attr = C.A_UNDERLINE + case COL_CURRENT_MATCH: + attr = C.A_UNDERLINE | C.A_REVERSE + } + if bold { + attr = attr | C.A_BOLD + } + return attr +} + +func MaxX() int { + return int(C.COLS) +} + +func MaxY() int { + return int(C.LINES) +} + +func getch(nonblock bool) int { + b := make([]byte, 1) + syscall.SetNonblock(int(_in.Fd()), nonblock) + _, err := _in.Read(b) + if err != nil { + return -1 + } + return int(b[0]) +} + +func Init(color bool, color256 bool, black bool, mouse bool) { + { + in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) + if err != nil { + panic("Failed to open /dev/tty") + } + _in = in + // Break STDIN + // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) + } + + swapOutput() + + C.setlocale(C.LC_ALL, C.CString("")) + C.initscr() + if mouse { + C.mousemask(C.ALL_MOUSE_EVENTS, nil) + } + C.cbreak() + C.noecho() + C.raw() // stty dsusp undef + C.set_tabsize(4) // FIXME + + intChan := make(chan os.Signal, 1) + signal.Notify(intChan, os.Interrupt, os.Kill) + go func() { + <-intChan + Close() + os.Exit(1) + }() + + if color { + C.start_color() + var bg C.short + if black { + bg = C.COLOR_BLACK + } else { + C.use_default_colors() + bg = -1 + } + if color256 { + C.init_pair(COL_PROMPT, 110, bg) + C.init_pair(COL_MATCH, 108, bg) + C.init_pair(COL_CURRENT, 254, 236) + C.init_pair(COL_CURRENT_MATCH, 151, 236) + C.init_pair(COL_SPINNER, 148, bg) + C.init_pair(COL_INFO, 144, bg) + C.init_pair(COL_CURSOR, 161, 236) + C.init_pair(COL_SELECTED, 168, 236) + } else { + C.init_pair(COL_PROMPT, C.COLOR_BLUE, bg) + C.init_pair(COL_MATCH, C.COLOR_GREEN, bg) + C.init_pair(COL_CURRENT, C.COLOR_YELLOW, C.COLOR_BLACK) + C.init_pair(COL_CURRENT_MATCH, C.COLOR_GREEN, C.COLOR_BLACK) + C.init_pair(COL_SPINNER, C.COLOR_GREEN, bg) + C.init_pair(COL_INFO, C.COLOR_WHITE, bg) + C.init_pair(COL_CURSOR, C.COLOR_RED, C.COLOR_BLACK) + C.init_pair(COL_SELECTED, C.COLOR_MAGENTA, C.COLOR_BLACK) + } + _color = attrColored + } else { + _color = attrMono + } +} + +func Close() { + C.endwin() + swapOutput() +} + +func swapOutput() { + syscall.Dup2(2, 3) + syscall.Dup2(1, 2) + syscall.Dup2(3, 1) +} + +func GetBytes() []byte { + c := getch(false) + _buf = append(_buf, byte(c)) + + for { + c = getch(true) + if c == -1 { + break + } + _buf = append(_buf, byte(c)) + } + + return _buf +} + +// 27 (91 79) 77 type x y +func mouseSequence(sz *int) Event { + if len(_buf) < 6 { + return Event{INVALID, 0, nil} + } + *sz = 6 + switch _buf[3] { + case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl + 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl + mod := _buf[3] >= 36 + down := _buf[3]%2 == 0 + x := int(_buf[4] - 33) + y := int(_buf[5] - 33) + double := false + if down { + now := time.Now() + if now.Sub(_prevDownTime) < DOUBLE_CLICK_DURATION { + _clickY = append(_clickY, y) + } else { + _clickY = []int{y} + } + _prevDownTime = now + } else { + if len(_clickY) > 1 && _clickY[0] == _clickY[1] && + time.Now().Sub(_prevDownTime) < DOUBLE_CLICK_DURATION { + double = true + } + } + return Event{MOUSE, 0, &MouseEvent{y, x, 0, down, double, mod}} + case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl + 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl + mod := _buf[3] >= 100 + s := 1 - int(_buf[3]%2)*2 + return Event{MOUSE, 0, &MouseEvent{0, 0, s, false, false, mod}} + } + return Event{INVALID, 0, nil} +} + +func escSequence(sz *int) Event { + if len(_buf) < 2 { + return Event{ESC, 0, nil} + } + *sz = 2 + switch _buf[1] { + case 98: + return Event{ALT_B, 0, nil} + case 100: + return Event{ALT_D, 0, nil} + case 102: + return Event{ALT_F, 0, nil} + case 127: + return Event{ALT_BS, 0, nil} + case 91, 79: + if len(_buf) < 3 { + return Event{INVALID, 0, nil} + } + *sz = 3 + switch _buf[2] { + case 68: + return Event{CTRL_B, 0, nil} + case 67: + return Event{CTRL_F, 0, nil} + case 66: + return Event{CTRL_J, 0, nil} + case 65: + return Event{CTRL_K, 0, nil} + case 90: + return Event{BTAB, 0, nil} + case 72: + return Event{CTRL_A, 0, nil} + case 70: + return Event{CTRL_E, 0, nil} + case 77: + return mouseSequence(sz) + case 49, 50, 51, 52, 53, 54: + if len(_buf) < 4 { + return Event{INVALID, 0, nil} + } + *sz = 4 + switch _buf[2] { + case 50: + return Event{INVALID, 0, nil} // INS + case 51: + return Event{DEL, 0, nil} + case 52: + return Event{CTRL_E, 0, nil} + case 53: + return Event{PGUP, 0, nil} + case 54: + return Event{PGDN, 0, nil} + case 49: + switch _buf[3] { + case 126: + return Event{CTRL_A, 0, nil} + case 59: + if len(_buf) != 6 { + return Event{INVALID, 0, nil} + } + *sz = 6 + switch _buf[4] { + case 50: + switch _buf[5] { + case 68: + return Event{CTRL_A, 0, nil} + case 67: + return Event{CTRL_E, 0, nil} + } + case 53: + switch _buf[5] { + case 68: + return Event{ALT_B, 0, nil} + case 67: + return Event{ALT_F, 0, nil} + } + } // _buf[4] + } // _buf[3] + } // _buf[2] + } // _buf[2] + } // _buf[1] + return Event{INVALID, 0, nil} +} + +func GetChar() Event { + if len(_buf) == 0 { + _buf = GetBytes() + } + if len(_buf) == 0 { + panic("Empty _buffer") + } + + sz := 1 + defer func() { + _buf = _buf[sz:] + }() + + switch _buf[0] { + case CTRL_C, CTRL_G, CTRL_Q: + return Event{CTRL_C, 0, nil} + case 127: + return Event{CTRL_H, 0, nil} + case ESC: + return escSequence(&sz) + } + + // CTRL-A ~ CTRL-Z + if _buf[0] <= CTRL_Z { + return Event{int(_buf[0]), 0, nil} + } + r, rsz := utf8.DecodeRune(_buf) + sz = rsz + return Event{RUNE, r, nil} +} + +func Move(y int, x int) { + C.move(C.int(y), C.int(x)) +} + +func MoveAndClear(y int, x int) { + Move(y, x) + C.clrtoeol() +} + +func Print(text string) { + C.addstr(C.CString(text)) +} + +func CPrint(pair int, bold bool, text string) { + attr := _color(pair, bold) + C.attron(attr) + Print(text) + C.attroff(attr) +} + +func Clear() { + C.clear() +} + +func Refresh() { + C.refresh() +} diff --git a/src/eventbox.go b/src/eventbox.go new file mode 100644 index 0000000..6685e7c --- /dev/null +++ b/src/eventbox.go @@ -0,0 +1,48 @@ +package fzf + +import "sync" + +type EventType int + +type Events map[EventType]interface{} + +type EventBox struct { + events Events + cond *sync.Cond +} + +func NewEventBox() *EventBox { + return &EventBox{make(Events), sync.NewCond(&sync.Mutex{})} +} + +func (b *EventBox) Wait(callback func(*Events)) { + b.cond.L.Lock() + defer b.cond.L.Unlock() + + if len(b.events) == 0 { + b.cond.Wait() + } + + callback(&b.events) +} + +func (b *EventBox) Set(event EventType, value interface{}) { + b.cond.L.Lock() + defer b.cond.L.Unlock() + b.events[event] = value + b.cond.Broadcast() +} + +// Unsynchronized; should be called within Wait routine +func (events *Events) Clear() { + for event := range *events { + delete(*events, event) + } +} + +func (b *EventBox) Peak(event EventType) bool { + b.cond.L.Lock() + defer b.cond.L.Unlock() + _, ok := b.events[event] + return ok +} diff --git a/src/fzf/main.go b/src/fzf/main.go new file mode 100644 index 0000000..29d4767 --- /dev/null +++ b/src/fzf/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/junegunn/fzf/src" + +func main() { + fzf.Run(fzf.ParseOptions()) +} diff --git a/src/item.go b/src/item.go new file mode 100644 index 0000000..b70da93 --- /dev/null +++ b/src/item.go @@ -0,0 +1,135 @@ +package fzf + +import ( + "fmt" + "sort" +) + +type Offset [2]int + +type Item struct { + text *string + origText *string + offsets []Offset + index int + rank Rank + transformed *Transformed +} + +type Rank [3]int + +var NilRank = Rank{-1, 0, 0} + +func (i *Item) Rank() Rank { + if i.rank[0] > 0 { + return i.rank + } + sort.Sort(ByOrder(i.offsets)) + matchlen := 0 + prevEnd := 0 + for _, offset := range i.offsets { + begin := offset[0] + end := offset[1] + if prevEnd > begin { + begin = prevEnd + } + if end > prevEnd { + prevEnd = end + } + if end > begin { + matchlen += end - begin + } + } + i.rank = Rank{matchlen, len(*i.text), i.index} + return i.rank +} + +func (i *Item) Print() { + if i.origText != nil { + fmt.Println(*i.origText) + } else { + fmt.Println(*i.text) + } +} + +type ByOrder []Offset + +func (a ByOrder) Len() int { + return len(a) +} + +func (a ByOrder) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByOrder) Less(i, j int) bool { + ioff := a[i] + joff := a[j] + return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1]) +} + +type ByRelevance []*Item + +func (a ByRelevance) Len() int { + return len(a) +} + +func (a ByRelevance) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByRelevance) Less(i, j int) bool { + irank := a[i].Rank() + jrank := a[j].Rank() + + return compareRanks(irank, jrank) +} + +func compareRanks(irank Rank, jrank Rank) bool { + for idx := range irank { + if irank[idx] < jrank[idx] { + return true + } else if irank[idx] > jrank[idx] { + return false + } + } + return true +} + +func SortMerge(partialResults [][]*Item) []*Item { + if len(partialResults) == 1 { + return partialResults[0] + } + + merged := []*Item{} + + for len(partialResults) > 0 { + minRank := Rank{0, 0, 0} + minIdx := -1 + + for idx, partialResult := range partialResults { + if len(partialResult) > 0 { + rank := partialResult[0].Rank() + if minIdx < 0 || compareRanks(rank, minRank) { + minRank = rank + minIdx = idx + } + } + } + + if minIdx >= 0 { + merged = append(merged, partialResults[minIdx][0]) + partialResults[minIdx] = partialResults[minIdx][1:] + } + + nonEmptyPartialResults := make([][]*Item, 0, len(partialResults)) + for _, partialResult := range partialResults { + if len(partialResult) > 0 { + nonEmptyPartialResults = append(nonEmptyPartialResults, partialResult) + } + } + partialResults = nonEmptyPartialResults + } + + return merged +} diff --git a/src/item_test.go b/src/item_test.go new file mode 100644 index 0000000..1e31629 --- /dev/null +++ b/src/item_test.go @@ -0,0 +1,78 @@ +package fzf + +import ( + "sort" + "testing" +) + +func TestOffsetSort(t *testing.T) { + offsets := []Offset{ + Offset{3, 5}, Offset{2, 7}, + Offset{1, 3}, Offset{2, 9}} + sort.Sort(ByOrder(offsets)) + + if offsets[0][0] != 1 || offsets[0][1] != 3 || + offsets[1][0] != 2 || offsets[1][1] != 7 || + offsets[2][0] != 2 || offsets[2][1] != 9 || + offsets[3][0] != 3 || offsets[3][1] != 5 { + t.Error("Invalid order:", offsets) + } +} + +func TestRankComparison(t *testing.T) { + if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}) || + !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}) || + !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}) || + !compareRanks(NilRank, Rank{0, 0, 0}) || + compareRanks(Rank{0, 0, 0}, NilRank) { + t.Error("Invalid order") + } +} + +// Match length, string length, index +func TestItemRank(t *testing.T) { + strs := []string{"foo", "foobar", "bar", "baz"} + item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}} + rank1 := item1.Rank() + if rank1[0] != 0 || rank1[1] != 3 || rank1[2] != 1 { + t.Error(item1.Rank()) + } + // Only differ in index + item2 := Item{text: &strs[0], index: 0, offsets: []Offset{}} + + items := []*Item{&item1, &item2} + sort.Sort(ByRelevance(items)) + if items[0] != &item2 || items[1] != &item1 { + t.Error(items) + } + + items = []*Item{&item2, &item1, &item1, &item2} + sort.Sort(ByRelevance(items)) + if items[0] != &item2 || items[1] != &item2 || + items[2] != &item1 || items[3] != &item1 { + t.Error(items) + } + + // Sort by relevance + item3 := Item{text: &strs[1], index: 2, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item4 := Item{text: &strs[1], index: 2, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item5 := Item{text: &strs[2], index: 2, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item6 := Item{text: &strs[2], index: 2, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} + sort.Sort(ByRelevance(items)) + if items[0] != &item2 || items[1] != &item1 || + items[2] != &item6 || items[3] != &item4 || + items[4] != &item5 || items[5] != &item3 { + t.Error(items) + } + + // Sort merged lists + lists := [][]*Item{ + []*Item{&item2, &item4, &item5}, []*Item{&item1, &item6}, []*Item{&item3}} + items = SortMerge(lists) + if items[0] != &item2 || items[1] != &item1 || + items[2] != &item6 || items[3] != &item4 || + items[4] != &item5 || items[5] != &item3 { + t.Error(items) + } +} diff --git a/src/matcher.go b/src/matcher.go new file mode 100644 index 0000000..363b07f --- /dev/null +++ b/src/matcher.go @@ -0,0 +1,215 @@ +package fzf + +import ( + "fmt" + "runtime" + "sort" + "time" +) + +type MatchRequest struct { + chunks []*Chunk + pattern *Pattern +} + +type Matcher struct { + patternBuilder func([]rune) *Pattern + sort bool + eventBox *EventBox + reqBox *EventBox + partitions int + queryCache QueryCache +} + +const ( + REQ_RETRY EventType = iota + REQ_RESET +) + +const ( + STAT_CANCELLED int = iota + STAT_QCH + STAT_CHUNKS +) + +const ( + PROGRESS_MIN_DURATION = 200 * time.Millisecond +) + +func NewMatcher(patternBuilder func([]rune) *Pattern, + sort bool, eventBox *EventBox) *Matcher { + return &Matcher{ + patternBuilder: patternBuilder, + sort: sort, + eventBox: eventBox, + reqBox: NewEventBox(), + partitions: runtime.NumCPU(), + queryCache: make(QueryCache)} +} + +func (m *Matcher) Loop() { + prevCount := 0 + + for { + var request MatchRequest + + m.reqBox.Wait(func(events *Events) { + for _, val := range *events { + switch val := val.(type) { + case MatchRequest: + request = val + default: + panic(fmt.Sprintf("Unexpected type: %T", val)) + } + } + events.Clear() + }) + + // Restart search + patternString := request.pattern.AsString() + allMatches := []*Item{} + cancelled := false + count := CountItems(request.chunks) + + foundCache := false + if count == prevCount { + // Look up queryCache + if cached, found := m.queryCache[patternString]; found { + foundCache = true + allMatches = cached + } + } else { + // Invalidate queryCache + prevCount = count + m.queryCache = make(QueryCache) + } + + if !foundCache { + allMatches, cancelled = m.scan(request, 0) + } + + if !cancelled { + m.queryCache[patternString] = allMatches + m.eventBox.Set(EVT_SEARCH_FIN, allMatches) + } + } +} + +func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk { + perSlice := len(chunks) / m.partitions + + // No need to parallelize + if perSlice == 0 { + return [][]*Chunk{chunks} + } + + slices := make([][]*Chunk, m.partitions) + for i := 0; i < m.partitions; i++ { + start := i * perSlice + end := start + perSlice + if i == m.partitions-1 { + end = len(chunks) + } + slices[i] = chunks[start:end] + } + return slices +} + +type partialResult struct { + index int + matches []*Item +} + +func (m *Matcher) scan(request MatchRequest, limit int) ([]*Item, bool) { + startedAt := time.Now() + + numChunks := len(request.chunks) + if numChunks == 0 { + return []*Item{}, false + } + pattern := request.pattern + empty := pattern.IsEmpty() + cancelled := NewAtomicBool(false) + + slices := m.sliceChunks(request.chunks) + numSlices := len(slices) + resultChan := make(chan partialResult, numSlices) + countChan := make(chan int, numSlices) + + for idx, chunks := range slices { + go func(idx int, chunks []*Chunk) { + sliceMatches := []*Item{} + for _, chunk := range chunks { + var matches []*Item + if empty { + matches = *chunk + } else { + matches = request.pattern.Match(chunk) + } + sliceMatches = append(sliceMatches, matches...) + if cancelled.Get() { + return + } + countChan <- len(sliceMatches) + } + if !empty && m.sort { + sort.Sort(ByRelevance(sliceMatches)) + } + resultChan <- partialResult{idx, sliceMatches} + }(idx, chunks) + } + + count := 0 + matchCount := 0 + for matchesInChunk := range countChan { + count += 1 + matchCount += matchesInChunk + + if limit > 0 && matchCount > limit { + return nil, true // For --select-1 and --exit-0 + } + + if count == numChunks { + break + } + + if !empty && m.reqBox.Peak(REQ_RESET) { + cancelled.Set(true) + return nil, true + } + + if time.Now().Sub(startedAt) > PROGRESS_MIN_DURATION { + m.eventBox.Set(EVT_SEARCH_PROGRESS, float32(count)/float32(numChunks)) + } + } + + partialResults := make([][]*Item, numSlices) + for range slices { + partialResult := <-resultChan + partialResults[partialResult.index] = partialResult.matches + } + + var allMatches []*Item + if empty || !m.sort { + allMatches = []*Item{} + for _, matches := range partialResults { + allMatches = append(allMatches, matches...) + } + } else { + allMatches = SortMerge(partialResults) + } + + return allMatches, false +} + +func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { + pattern := m.patternBuilder(patternRunes) + + var event EventType + if cancel { + event = REQ_RESET + } else { + event = REQ_RETRY + } + m.reqBox.Set(event, MatchRequest{chunks, pattern}) +} diff --git a/src/options.go b/src/options.go new file mode 100644 index 0000000..4929dfd --- /dev/null +++ b/src/options.go @@ -0,0 +1,276 @@ +package fzf + +import ( + "fmt" + "github.com/junegunn/go-shellwords" + "os" + "regexp" + "strings" +) + +const 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]) + --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 Sort the result + +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: '> ') + + 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 + + Environment variables + FZF_DEFAULT_COMMAND Default command to use when input is tty + FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m") + +` + +type Mode int + +const ( + MODE_FUZZY Mode = iota + MODE_EXTENDED + MODE_EXTENDED_EXACT +) + +type Case int + +const ( + CASE_SMART Case = iota + CASE_IGNORE + CASE_RESPECT +) + +type Options struct { + Mode Mode + Case Case + Nth []Range + WithNth []Range + Delimiter *regexp.Regexp + Sort int + Multi bool + Mouse bool + Color bool + Color256 bool + Black bool + Reverse bool + Prompt string + Query string + Select1 bool + Exit0 bool + Filter *string + PrintQuery bool + Version bool +} + +func DefaultOptions() *Options { + return &Options{ + Mode: MODE_FUZZY, + Case: CASE_SMART, + Nth: make([]Range, 0), + WithNth: make([]Range, 0), + Delimiter: nil, + Sort: 1000, + Multi: false, + Mouse: true, + Color: true, + Color256: strings.Contains(os.Getenv("TERM"), "256"), + Black: false, + Reverse: false, + Prompt: "> ", + Query: "", + Select1: false, + Exit0: false, + Filter: nil, + PrintQuery: false, + Version: false} +} + +func help(ok int) { + os.Stderr.WriteString(USAGE) + os.Exit(ok) +} + +func errorExit(msg string) { + os.Stderr.WriteString(msg + "\n") + help(1) +} + +func optString(arg string, prefix string) (bool, string) { + rx, _ := regexp.Compile(fmt.Sprintf("^(?:%s)(.*)$", prefix)) + matches := rx.FindStringSubmatch(arg) + if len(matches) > 1 { + return true, matches[1] + } else { + return false, "" + } +} + +func nextString(args []string, i *int, message string) string { + if len(args) > *i+1 { + *i++ + } else { + errorExit(message) + } + return args[*i] +} + +func optionalNumeric(args []string, i *int) int { + if len(args) > *i+1 { + if strings.IndexAny(args[*i+1], "0123456789") == 0 { + *i++ + } + } + return 1 // Don't care +} + +func splitNth(str string) []Range { + if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match { + errorExit("invalid format: " + str) + } + + tokens := strings.Split(str, ",") + ranges := make([]Range, len(tokens)) + for idx, s := range tokens { + r, ok := ParseRange(&s) + if !ok { + errorExit("invalid format: " + str) + } + ranges[idx] = r + } + return ranges +} + +func delimiterRegexp(str string) *regexp.Regexp { + rx, e := regexp.Compile(str) + if e != nil { + str = regexp.QuoteMeta(str) + } + + rx, e = regexp.Compile(fmt.Sprintf("(?:.*?%s)|(?:.+?$)", str)) + if e != nil { + errorExit("invalid regular expression: " + e.Error()) + } + return rx +} + +func parseOptions(opts *Options, allArgs []string) { + for i := 0; i < len(allArgs); i++ { + arg := allArgs[i] + switch arg { + case "-h", "--help": + help(0) + case "-x", "--extended": + opts.Mode = MODE_EXTENDED + case "-e", "--extended-exact": + opts.Mode = MODE_EXTENDED_EXACT + case "+x", "--no-extended", "+e", "--no-extended-exact": + opts.Mode = MODE_FUZZY + case "-q", "--query": + opts.Query = nextString(allArgs, &i, "query string required") + case "-f", "--filter": + filter := nextString(allArgs, &i, "query string required") + opts.Filter = &filter + case "-d", "--delimiter": + opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) + case "-n", "--nth": + opts.Nth = splitNth(nextString(allArgs, &i, "nth expression required")) + case "--with-nth": + opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required")) + case "-s", "--sort": + opts.Sort = optionalNumeric(allArgs, &i) + case "+s", "--no-sort": + opts.Sort = 0 + case "-i": + opts.Case = CASE_IGNORE + case "+i": + opts.Case = CASE_RESPECT + case "-m", "--multi": + opts.Multi = true + case "+m", "--no-multi": + opts.Multi = false + case "--no-mouse": + opts.Mouse = false + case "+c", "--no-color": + opts.Color = false + case "+2", "--no-256": + opts.Color256 = false + case "--black": + opts.Black = true + case "--no-black": + opts.Black = false + case "--reverse": + opts.Reverse = true + case "--no-reverse": + opts.Reverse = false + case "-1", "--select-1": + opts.Select1 = true + case "+1", "--no-select-1": + opts.Select1 = false + case "-0", "--exit-0": + opts.Exit0 = true + case "+0", "--no-exit-0": + opts.Exit0 = false + case "--print-query": + opts.PrintQuery = true + case "--no-print-query": + opts.PrintQuery = false + case "--prompt": + opts.Prompt = nextString(allArgs, &i, "prompt string required") + case "--version": + opts.Version = true + default: + if match, value := optString(arg, "-q|--query="); match { + opts.Query = value + } else if match, value := optString(arg, "-f|--filter="); match { + opts.Filter = &value + } else if match, value := optString(arg, "-d|--delimiter="); match { + opts.Delimiter = delimiterRegexp(value) + } else if match, value := optString(arg, "--prompt="); match { + opts.Prompt = value + } else if match, value := optString(arg, "-n|--nth="); match { + opts.Nth = splitNth(value) + } else if match, value := optString(arg, "--with-nth="); match { + opts.WithNth = splitNth(value) + } else if match, _ := optString(arg, "-s|--sort="); match { + opts.Sort = 1 // Don't care + } else { + errorExit("unknown option: " + arg) + } + } + } +} + +func ParseOptions() *Options { + opts := DefaultOptions() + + // Options from Env var + words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS")) + parseOptions(opts, words) + + // Options from command-line arguments + parseOptions(opts, os.Args[1:]) + return opts +} diff --git a/src/options_test.go b/src/options_test.go new file mode 100644 index 0000000..f0aa3a0 --- /dev/null +++ b/src/options_test.go @@ -0,0 +1,37 @@ +package fzf + +import "testing" + +func TestDelimiterRegex(t *testing.T) { + rx := delimiterRegexp("*") + tokens := rx.FindAllString("-*--*---**---", -1) + if tokens[0] != "-*" || tokens[1] != "--*" || tokens[2] != "---*" || + tokens[3] != "*" || tokens[4] != "---" { + t.Errorf("%s %s %d", rx, tokens, len(tokens)) + } +} + +func TestSplitNth(t *testing.T) { + { + ranges := splitNth("..") + if len(ranges) != 1 || + ranges[0].begin != RANGE_ELLIPSIS || + ranges[0].end != RANGE_ELLIPSIS { + t.Errorf("%s", ranges) + } + } + { + ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2") + if len(ranges) != 8 || + ranges[0].begin != RANGE_ELLIPSIS || ranges[0].end != 3 || + ranges[1].begin != 1 || ranges[1].end != RANGE_ELLIPSIS || + ranges[2].begin != 2 || ranges[2].end != 3 || + ranges[3].begin != 4 || ranges[3].end != -1 || + ranges[4].begin != -3 || ranges[4].end != -2 || + ranges[5].begin != RANGE_ELLIPSIS || ranges[5].end != RANGE_ELLIPSIS || + ranges[6].begin != 2 || ranges[6].end != 2 || + ranges[7].begin != -2 || ranges[7].end != -2 { + t.Errorf("%s", ranges) + } + } +} diff --git a/src/pattern.go b/src/pattern.go new file mode 100644 index 0000000..533aa59 --- /dev/null +++ b/src/pattern.go @@ -0,0 +1,305 @@ +package fzf + +import ( + "regexp" + "sort" + "strings" +) + +const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +// fuzzy +// 'exact +// ^exact-prefix +// exact-suffix$ +// !not-fuzzy +// !'not-exact +// !^not-exact-prefix +// !not-exact-suffix$ + +type TermType int + +const ( + TERM_FUZZY TermType = iota + TERM_EXACT + TERM_PREFIX + TERM_SUFFIX +) + +type Term struct { + typ TermType + inv bool + text []rune + origText []rune +} + +type Pattern struct { + mode Mode + caseSensitive bool + text []rune + terms []Term + hasInvTerm bool + delimiter *regexp.Regexp + nth []Range + procFun map[TermType]func(bool, *string, []rune) (int, int) +} + +var ( + _patternCache map[string]*Pattern + _splitRegex *regexp.Regexp + _cache ChunkCache +) + +func init() { + // We can uniquely identify the pattern for a given string since + // mode and caseMode do not change while the program is running + _patternCache = make(map[string]*Pattern) + _splitRegex = regexp.MustCompile("\\s+") + _cache = NewChunkCache() +} + +func clearPatternCache() { + _patternCache = make(map[string]*Pattern) +} + +func BuildPattern(mode Mode, caseMode Case, + nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern { + + var asString string + switch mode { + case MODE_EXTENDED, MODE_EXTENDED_EXACT: + asString = strings.Trim(string(runes), " ") + default: + asString = string(runes) + } + + cached, found := _patternCache[asString] + if found { + return cached + } + + caseSensitive, hasInvTerm := true, false + terms := []Term{} + + switch caseMode { + case CASE_SMART: + if !strings.ContainsAny(asString, UPPERCASE) { + runes, caseSensitive = []rune(strings.ToLower(asString)), false + } + case CASE_IGNORE: + runes, caseSensitive = []rune(strings.ToLower(asString)), false + } + + switch mode { + case MODE_EXTENDED, MODE_EXTENDED_EXACT: + terms = parseTerms(mode, string(runes)) + for _, term := range terms { + if term.inv { + hasInvTerm = true + } + } + } + + ptr := &Pattern{ + mode: mode, + caseSensitive: caseSensitive, + text: runes, + terms: terms, + hasInvTerm: hasInvTerm, + nth: nth, + delimiter: delimiter, + procFun: make(map[TermType]func(bool, *string, []rune) (int, int))} + + ptr.procFun[TERM_FUZZY] = FuzzyMatch + ptr.procFun[TERM_EXACT] = ExactMatchNaive + ptr.procFun[TERM_PREFIX] = PrefixMatch + ptr.procFun[TERM_SUFFIX] = SuffixMatch + + _patternCache[asString] = ptr + return ptr +} + +func parseTerms(mode Mode, str string) []Term { + tokens := _splitRegex.Split(str, -1) + terms := []Term{} + for _, token := range tokens { + typ, inv, text := TERM_FUZZY, false, token + origText := []rune(text) + if mode == MODE_EXTENDED_EXACT { + typ = TERM_EXACT + } + + if strings.HasPrefix(text, "!") { + inv = true + text = text[1:] + } + + if strings.HasPrefix(text, "'") { + if mode == MODE_EXTENDED { + typ = TERM_EXACT + text = text[1:] + } + } else if strings.HasPrefix(text, "^") { + typ = TERM_PREFIX + text = text[1:] + } else if strings.HasSuffix(text, "$") { + typ = TERM_SUFFIX + text = text[:len(text)-1] + } + + if len(text) > 0 { + terms = append(terms, Term{ + typ: typ, + inv: inv, + text: []rune(text), + origText: origText}) + } + } + return terms +} + +func (p *Pattern) IsEmpty() bool { + if p.mode == MODE_FUZZY { + return len(p.text) == 0 + } else { + return len(p.terms) == 0 + } +} + +func (p *Pattern) AsString() string { + return string(p.text) +} + +func (p *Pattern) CacheKey() string { + if p.mode == MODE_FUZZY { + return p.AsString() + } + cacheableTerms := []string{} + for _, term := range p.terms { + if term.inv { + continue + } + cacheableTerms = append(cacheableTerms, string(term.origText)) + } + sort.Strings(cacheableTerms) + return strings.Join(cacheableTerms, " ") +} + +func (p *Pattern) Match(chunk *Chunk) []*Item { + space := chunk + + // ChunkCache: Exact match + cacheKey := p.CacheKey() + if !p.hasInvTerm { // Because we're excluding Inv-term from cache key + if cached, found := _cache.Find(chunk, cacheKey); found { + return cached + } + } + + // ChunkCache: Prefix match + foundPrefixCache := false + for idx := len(cacheKey) - 1; idx > 0; idx-- { + if cached, found := _cache.Find(chunk, cacheKey[:idx]); found { + cachedChunk := Chunk(cached) + space = &cachedChunk + foundPrefixCache = true + break + } + } + + // ChunkCache: Suffix match + if !foundPrefixCache { + for idx := 1; idx < len(cacheKey); idx++ { + if cached, found := _cache.Find(chunk, cacheKey[idx:]); found { + cachedChunk := Chunk(cached) + space = &cachedChunk + break + } + } + } + + var matches []*Item + if p.mode == MODE_FUZZY { + matches = p.fuzzyMatch(space) + } else { + matches = p.extendedMatch(space) + } + + if !p.hasInvTerm { + _cache.Add(chunk, cacheKey, matches) + } + return matches +} + +func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { + matches := []*Item{} + for _, item := range *chunk { + input := p.prepareInput(item) + if sidx, eidx := p.iter(FuzzyMatch, input, p.text); sidx >= 0 { + matches = append(matches, &Item{ + text: item.text, + index: item.index, + offsets: []Offset{Offset{sidx, eidx}}, + rank: NilRank}) + } + } + return matches +} + +func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { + matches := []*Item{} + for _, item := range *chunk { + input := p.prepareInput(item) + offsets := []Offset{} + Loop: + for _, term := range p.terms { + pfun := p.procFun[term.typ] + if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { + if term.inv { + break Loop + } + offsets = append(offsets, Offset{sidx, eidx}) + } else if term.inv { + offsets = append(offsets, Offset{0, 0}) + } + } + if len(offsets) == len(p.terms) { + matches = append(matches, &Item{ + text: item.text, + index: item.index, + offsets: offsets, + rank: NilRank}) + } + } + return matches +} + +func (p *Pattern) prepareInput(item *Item) *Transformed { + if item.transformed != nil { + return item.transformed + } + + var ret *Transformed + if len(p.nth) > 0 { + tokens := Tokenize(item.text, p.delimiter) + ret = Transform(tokens, p.nth) + } else { + trans := Transformed{ + whole: item.text, + parts: []Token{Token{text: item.text, prefixLength: 0}}} + ret = &trans + } + item.transformed = ret + return ret +} + +func (p *Pattern) iter(pfun func(bool, *string, []rune) (int, int), + inputs *Transformed, pattern []rune) (int, int) { + for _, part := range inputs.parts { + prefixLength := part.prefixLength + if sidx, eidx := pfun(p.caseSensitive, part.text, pattern); sidx >= 0 { + return sidx + prefixLength, eidx + prefixLength + } + } + return -1, -1 +} diff --git a/src/pattern_test.go b/src/pattern_test.go new file mode 100644 index 0000000..a1ce626 --- /dev/null +++ b/src/pattern_test.go @@ -0,0 +1,87 @@ +package fzf + +import "testing" + +func TestParseTermsExtended(t *testing.T) { + terms := parseTerms(MODE_EXTENDED, + "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") + if len(terms) != 8 || + terms[0].typ != TERM_FUZZY || terms[0].inv || + terms[1].typ != TERM_EXACT || terms[1].inv || + terms[2].typ != TERM_PREFIX || terms[2].inv || + terms[3].typ != TERM_SUFFIX || terms[3].inv || + terms[4].typ != TERM_FUZZY || !terms[4].inv || + terms[5].typ != TERM_EXACT || !terms[5].inv || + terms[6].typ != TERM_PREFIX || !terms[6].inv || + terms[7].typ != TERM_SUFFIX || !terms[7].inv { + t.Errorf("%s", terms) + } + for idx, term := range terms { + if len(term.text) != 3 { + t.Errorf("%s", term) + } + if idx > 0 && len(term.origText) != 4+idx/5 { + t.Errorf("%s", term) + } + } +} + +func TestParseTermsExtendedExact(t *testing.T) { + terms := parseTerms(MODE_EXTENDED_EXACT, + "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") + if len(terms) != 8 || + terms[0].typ != TERM_EXACT || terms[0].inv || len(terms[0].text) != 3 || + terms[1].typ != TERM_EXACT || terms[1].inv || len(terms[1].text) != 4 || + terms[2].typ != TERM_PREFIX || terms[2].inv || len(terms[2].text) != 3 || + terms[3].typ != TERM_SUFFIX || terms[3].inv || len(terms[3].text) != 3 || + terms[4].typ != TERM_EXACT || !terms[4].inv || len(terms[4].text) != 3 || + terms[5].typ != TERM_EXACT || !terms[5].inv || len(terms[5].text) != 4 || + terms[6].typ != TERM_PREFIX || !terms[6].inv || len(terms[6].text) != 3 || + terms[7].typ != TERM_SUFFIX || !terms[7].inv || len(terms[7].text) != 3 { + t.Errorf("%s", terms) + } +} + +func TestParseTermsEmpty(t *testing.T) { + terms := parseTerms(MODE_EXTENDED, "' $ ^ !' !^ !$") + if len(terms) != 0 { + t.Errorf("%s", terms) + } +} + +func TestExact(t *testing.T) { + defer clearPatternCache() + clearPatternCache() + pattern := BuildPattern(MODE_EXTENDED, CASE_SMART, + []Range{}, nil, []rune("'abc")) + str := "aabbcc abc" + sidx, eidx := ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text) + if sidx != 7 || eidx != 10 { + t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) + } +} + +func TestCaseSensitivity(t *testing.T) { + defer clearPatternCache() + clearPatternCache() + pat1 := BuildPattern(MODE_FUZZY, CASE_SMART, []Range{}, nil, []rune("abc")) + clearPatternCache() + pat2 := BuildPattern(MODE_FUZZY, CASE_SMART, []Range{}, nil, []rune("Abc")) + clearPatternCache() + pat3 := BuildPattern(MODE_FUZZY, CASE_IGNORE, []Range{}, nil, []rune("abc")) + clearPatternCache() + pat4 := BuildPattern(MODE_FUZZY, CASE_IGNORE, []Range{}, nil, []rune("Abc")) + clearPatternCache() + pat5 := BuildPattern(MODE_FUZZY, CASE_RESPECT, []Range{}, nil, []rune("abc")) + clearPatternCache() + pat6 := BuildPattern(MODE_FUZZY, CASE_RESPECT, []Range{}, nil, []rune("Abc")) + + if string(pat1.text) != "abc" || pat1.caseSensitive != false || + string(pat2.text) != "Abc" || pat2.caseSensitive != true || + string(pat3.text) != "abc" || pat3.caseSensitive != false || + string(pat4.text) != "abc" || pat4.caseSensitive != false || + string(pat5.text) != "abc" || pat5.caseSensitive != true || + string(pat6.text) != "Abc" || pat6.caseSensitive != true { + t.Error("Invalid case conversion") + } +} diff --git a/src/reader.go b/src/reader.go new file mode 100644 index 0000000..0e1f0a9 --- /dev/null +++ b/src/reader.go @@ -0,0 +1,60 @@ +package fzf + +// #include +import "C" + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" +) + +const DEFAULT_COMMAND = "find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null" + +type Reader struct { + pusher func(string) + eventBox *EventBox +} + +func (r *Reader) ReadSource() { + if int(C.isatty(C.int(os.Stdin.Fd()))) != 0 { + cmd := os.Getenv("FZF_DEFAULT_COMMAND") + if len(cmd) == 0 { + cmd = DEFAULT_COMMAND + } + r.readFromCommand(cmd) + } else { + r.readFromStdin() + } + r.eventBox.Set(EVT_READ_FIN, nil) +} + +func (r *Reader) feed(src io.Reader) { + if scanner := bufio.NewScanner(src); scanner != nil { + for scanner.Scan() { + r.pusher(scanner.Text()) + r.eventBox.Set(EVT_READ_NEW, nil) + } + } +} + +func (r *Reader) readFromStdin() { + r.feed(os.Stdin) +} + +func (r *Reader) readFromCommand(cmd string) { + arg := fmt.Sprintf("%q", cmd) + listCommand := exec.Command("sh", "-c", arg[1:len(arg)-1]) + out, err := listCommand.StdoutPipe() + if err != nil { + return + } + err = listCommand.Start() + if err != nil { + return + } + defer listCommand.Wait() + r.feed(out) +} diff --git a/src/reader_test.go b/src/reader_test.go new file mode 100644 index 0000000..f51ccab --- /dev/null +++ b/src/reader_test.go @@ -0,0 +1,52 @@ +package fzf + +import "testing" + +func TestReadFromCommand(t *testing.T) { + strs := []string{} + eb := NewEventBox() + reader := Reader{ + pusher: func(s string) { strs = append(strs, s) }, + eventBox: eb} + + // Check EventBox + if eb.Peak(EVT_READ_NEW) { + t.Error("EVT_READ_NEW should not be set yet") + } + + // Normal command + reader.readFromCommand(`echo abc && echo def`) + if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { + t.Errorf("%s", strs) + } + + // Check EventBox again + if !eb.Peak(EVT_READ_NEW) { + t.Error("EVT_READ_NEW should be set yet") + } + + // Wait should return immediately + eb.Wait(func(events *Events) { + if _, found := (*events)[EVT_READ_NEW]; !found { + t.Errorf("%s", events) + } + events.Clear() + }) + + // EventBox is cleared + if eb.Peak(EVT_READ_NEW) { + t.Error("EVT_READ_NEW should not be set yet") + } + + // Failing command + reader.readFromCommand(`no-such-command`) + strs = []string{} + if len(strs) > 0 { + t.Errorf("%s", strs) + } + + // Check EventBox again + if eb.Peak(EVT_READ_NEW) { + t.Error("Command failed. EVT_READ_NEW should be set") + } +} diff --git a/src/terminal.go b/src/terminal.go new file mode 100644 index 0000000..b6c7154 --- /dev/null +++ b/src/terminal.go @@ -0,0 +1,580 @@ +package fzf + +import ( + "fmt" + C "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/go-runewidth" + "os" + "regexp" + "sort" + "sync" + "time" +) + +type Terminal struct { + prompt string + reverse bool + tac bool + cx int + cy int + offset int + yanked []rune + input []rune + multi bool + printQuery bool + count int + progress int + reading bool + list []*Item + selected map[*string]*string + reqBox *EventBox + eventBox *EventBox + mutex sync.Mutex + initFunc func() +} + +var _spinner []string = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} + +const ( + REQ_PROMPT EventType = iota + REQ_INFO + REQ_LIST + REQ_REDRAW + REQ_CLOSE + REQ_QUIT +) + +func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { + input := []rune(opts.Query) + return &Terminal{ + prompt: opts.Prompt, + tac: opts.Sort == 0, + reverse: opts.Reverse, + cx: displayWidth(input), + cy: 0, + offset: 0, + yanked: []rune{}, + input: input, + multi: opts.Multi, + printQuery: opts.PrintQuery, + list: []*Item{}, + selected: make(map[*string]*string), + reqBox: NewEventBox(), + eventBox: eventBox, + mutex: sync.Mutex{}, + initFunc: func() { + C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse) + }} +} + +func (t *Terminal) Input() []rune { + t.mutex.Lock() + defer t.mutex.Unlock() + return copySlice(t.input) +} + +func (t *Terminal) UpdateCount(cnt int, final bool) { + t.mutex.Lock() + t.count = cnt + t.reading = !final + t.mutex.Unlock() + t.reqBox.Set(REQ_INFO, nil) +} + +func (t *Terminal) UpdateProgress(progress float32) { + t.mutex.Lock() + t.progress = int(progress * 100) + t.mutex.Unlock() + t.reqBox.Set(REQ_INFO, nil) +} + +func (t *Terminal) UpdateList(list []*Item) { + t.mutex.Lock() + t.progress = 100 + t.list = list + t.mutex.Unlock() + t.reqBox.Set(REQ_INFO, nil) + t.reqBox.Set(REQ_LIST, nil) +} + +func (t *Terminal) listIndex(y int) int { + if t.tac { + return len(t.list) - y - 1 + } else { + return y + } +} + +func (t *Terminal) output() { + if t.printQuery { + fmt.Println(string(t.input)) + } + if len(t.selected) == 0 { + if len(t.list) > t.cy { + t.list[t.listIndex(t.cy)].Print() + } + } else { + for ptr, orig := range t.selected { + if orig != nil { + fmt.Println(*orig) + } else { + fmt.Println(*ptr) + } + } + } +} + +func displayWidth(runes []rune) int { + l := 0 + for _, r := range runes { + l += runewidth.RuneWidth(r) + } + return l +} + +func (t *Terminal) move(y int, x int, clear bool) { + maxy := C.MaxY() + if !t.reverse { + y = maxy - y - 1 + } + + if clear { + C.MoveAndClear(y, x) + } else { + C.Move(y, x) + } +} + +func (t *Terminal) placeCursor() { + t.move(0, len(t.prompt)+displayWidth(t.input[:t.cx]), false) +} + +func (t *Terminal) printPrompt() { + t.move(0, 0, true) + C.CPrint(C.COL_PROMPT, true, t.prompt) + C.CPrint(C.COL_NORMAL, true, string(t.input)) +} + +func (t *Terminal) printInfo() { + t.move(1, 0, true) + if t.reading { + duration := int64(200) * int64(time.Millisecond) + idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration + C.CPrint(C.COL_SPINNER, true, _spinner[idx]) + } + + t.move(1, 2, false) + output := fmt.Sprintf("%d/%d", len(t.list), t.count) + if t.multi && len(t.selected) > 0 { + output += fmt.Sprintf(" (%d)", len(t.selected)) + } + if t.progress > 0 && t.progress < 100 { + output += fmt.Sprintf(" (%d%%)", t.progress) + } + C.CPrint(C.COL_INFO, false, output) +} + +func (t *Terminal) printList() { + t.constrain() + + maxy := maxItems() + count := len(t.list) - t.offset + for i := 0; i < maxy; i++ { + t.move(i+2, 0, true) + if i < count { + t.printItem(t.list[t.listIndex(i+t.offset)], i == t.cy-t.offset) + } + } +} + +func (t *Terminal) printItem(item *Item, current bool) { + _, selected := t.selected[item.text] + if current { + C.CPrint(C.COL_CURSOR, true, ">") + if selected { + C.CPrint(C.COL_CURRENT, true, ">") + } else { + C.CPrint(C.COL_CURRENT, true, " ") + } + t.printHighlighted(item, true, C.COL_CURRENT, C.COL_CURRENT_MATCH) + } else { + C.CPrint(C.COL_CURSOR, true, " ") + if selected { + C.CPrint(C.COL_SELECTED, true, ">") + } else { + C.Print(" ") + } + t.printHighlighted(item, false, 0, C.COL_MATCH) + } +} + +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 += 1 + } + return runes, trimmed +} + +func trimLeft(runes []rune, width int) ([]rune, int) { + currentWidth := displayWidth(runes) + trimmed := 0 + + for currentWidth > width && len(runes) > 0 { + currentWidth -= runewidth.RuneWidth(runes[0]) + runes = runes[1:] + trimmed += 1 + } + return runes, trimmed +} + +func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { + maxe := 0 + for _, offset := range item.offsets { + if offset[1] > maxe { + maxe = offset[1] + } + } + + // Overflow + text := []rune(*item.text) + offsets := item.offsets + maxWidth := C.MaxX() - 3 + fullWidth := displayWidth(text) + if fullWidth > maxWidth { + // Stri.. + matchEndWidth := displayWidth(text[:maxe]) + if matchEndWidth <= maxWidth-2 { + text, _ = trimRight(text, maxWidth-2) + text = append(text, []rune("..")...) + } else { + // Stri.. + if matchEndWidth < fullWidth-2 { + text = append(text[:maxe], []rune("..")...) + } + // ..ri.. + var diff int + text, diff = trimLeft(text, maxWidth-2) + + // Transform offsets + offsets = make([]Offset, len(item.offsets)) + for idx, offset := range item.offsets { + b, e := offset[0], offset[1] + b += 2 - diff + e += 2 - diff + b = Max(b, 2) + if b < e { + offsets[idx] = Offset{b, e} + } + } + text = append([]rune(".."), text...) + } + } + + sort.Sort(ByOrder(offsets)) + index := 0 + for _, offset := range offsets { + b := Max(index, offset[0]) + e := Max(index, offset[1]) + C.CPrint(col1, bold, string(text[index:b])) + C.CPrint(col2, bold, string(text[b:e])) + index = e + } + if index < len(text) { + C.CPrint(col1, bold, string(text[index:])) + } +} + +func (t *Terminal) printAll() { + t.printList() + t.printInfo() + t.printPrompt() +} + +func (t *Terminal) refresh() { + t.placeCursor() + C.Refresh() +} + +func (t *Terminal) delChar() bool { + if len(t.input) > 0 && t.cx < len(t.input) { + t.input = append(t.input[:t.cx], t.input[t.cx+1:]...) + return true + } + return false +} + +func findLastMatch(pattern string, str string) int { + rx, err := regexp.Compile(pattern) + if err != nil { + return -1 + } + locs := rx.FindAllStringIndex(str, -1) + if locs == nil { + return -1 + } + return locs[len(locs)-1][0] +} + +func findFirstMatch(pattern string, str string) int { + rx, err := regexp.Compile(pattern) + if err != nil { + return -1 + } + loc := rx.FindStringIndex(str) + if loc == nil { + return -1 + } + return loc[0] +} + +func copySlice(slice []rune) []rune { + ret := make([]rune, len(slice)) + copy(ret, slice) + return ret +} + +func (t *Terminal) rubout(pattern string) { + pcx := t.cx + after := t.input[t.cx:] + t.cx = findLastMatch(pattern, string(t.input[:t.cx])) + 1 + t.yanked = copySlice(t.input[t.cx:pcx]) + t.input = append(t.input[:t.cx], after...) +} + +func (t *Terminal) Loop() { + { // Late initialization + t.mutex.Lock() + t.initFunc() + t.printInfo() + t.printPrompt() + t.refresh() + t.mutex.Unlock() + } + + go func() { + for { + t.reqBox.Wait(func(events *Events) { + defer events.Clear() + t.mutex.Lock() + for req := range *events { + switch req { + case REQ_PROMPT: + t.printPrompt() + case REQ_INFO: + t.printInfo() + case REQ_LIST: + t.printList() + case REQ_REDRAW: + C.Clear() + t.printAll() + case REQ_CLOSE: + C.Close() + t.output() + os.Exit(0) + case REQ_QUIT: + C.Close() + os.Exit(1) + } + } + t.mutex.Unlock() + }) + t.refresh() + } + }() + + looping := true + for looping { + event := C.GetChar() + + t.mutex.Lock() + previousInput := t.input + events := []EventType{REQ_PROMPT} + toggle := func() { + item := t.list[t.listIndex(t.cy)] + if _, found := t.selected[item.text]; !found { + t.selected[item.text] = item.origText + } else { + delete(t.selected, item.text) + } + } + req := func(evts ...EventType) { + for _, event := range evts { + events = append(events, event) + if event == REQ_CLOSE || event == REQ_QUIT { + looping = false + } + } + } + switch event.Type { + case C.INVALID: + continue + case C.CTRL_A: + t.cx = 0 + case C.CTRL_B: + if t.cx > 0 { + t.cx -= 1 + } + case C.CTRL_C, C.CTRL_G, C.CTRL_Q, C.ESC: + req(REQ_QUIT) + case C.CTRL_D: + if !t.delChar() && t.cx == 0 { + req(REQ_QUIT) + } + case C.CTRL_E: + t.cx = len(t.input) + case C.CTRL_F: + if t.cx < len(t.input) { + t.cx += 1 + } + case C.CTRL_H: + if t.cx > 0 { + t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) + t.cx -= 1 + } + case C.TAB: + if t.multi && len(t.list) > 0 { + toggle() + t.vmove(-1) + req(REQ_LIST, REQ_INFO) + } + case C.BTAB: + if t.multi && len(t.list) > 0 { + toggle() + t.vmove(1) + req(REQ_LIST, REQ_INFO) + } + case C.CTRL_J, C.CTRL_N: + t.vmove(-1) + req(REQ_LIST) + case C.CTRL_K, C.CTRL_P: + t.vmove(1) + req(REQ_LIST) + case C.CTRL_M: + req(REQ_CLOSE) + case C.CTRL_L: + req(REQ_REDRAW) + case C.CTRL_U: + if t.cx > 0 { + t.yanked = copySlice(t.input[:t.cx]) + t.input = t.input[t.cx:] + t.cx = 0 + } + case C.CTRL_W: + if t.cx > 0 { + t.rubout("\\s\\S") + } + case C.ALT_BS: + if t.cx > 0 { + t.rubout("[^[:alnum:]][[:alnum:]]") + } + case C.CTRL_Y: + t.input = append(append(t.input[:t.cx], t.yanked...), t.input[t.cx:]...) + t.cx += len(t.yanked) + case C.DEL: + t.delChar() + case C.PGUP: + t.vmove(maxItems() - 1) + req(REQ_LIST) + case C.PGDN: + t.vmove(-(maxItems() - 1)) + req(REQ_LIST) + case C.ALT_B: + t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 + case C.ALT_F: + t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 + case C.ALT_D: + ncx := t.cx + + findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 + if ncx > t.cx { + t.yanked = copySlice(t.input[t.cx:ncx]) + t.input = append(t.input[:t.cx], t.input[ncx:]...) + } + case C.RUNE: + prefix := copySlice(t.input[:t.cx]) + t.input = append(append(prefix, event.Char), t.input[t.cx:]...) + t.cx += 1 + case C.MOUSE: + me := event.MouseEvent + mx, my := Min(len(t.input), Max(0, me.X-len(t.prompt))), me.Y + if !t.reverse { + my = C.MaxY() - my - 1 + } + if me.S != 0 { + // Scroll + if me.Mod { + toggle() + } + t.vmove(me.S) + req(REQ_LIST) + } else if me.Double { + // Double-click + if my >= 2 { + t.cy = my - 2 + req(REQ_CLOSE) + } + } else if me.Down { + if my == 0 && mx >= 0 { + // Prompt + t.cx = mx + req(REQ_PROMPT) + } else if my >= 2 { + // List + t.cy = my - 2 + if me.Mod { + toggle() + } + req(REQ_LIST) + } + } + } + changed := string(previousInput) != string(t.input) + t.mutex.Unlock() // Must be unlocked before touching reqBox + + if changed { + t.eventBox.Set(EVT_SEARCH_NEW, nil) + } + for _, event := range events { + t.reqBox.Set(event, nil) + } + } +} + +func (t *Terminal) constrain() { + count := len(t.list) + height := C.MaxY() - 2 + diffpos := t.cy - t.offset + + t.cy = Max(0, Min(t.cy, count-1)) + + if t.cy > t.offset+(height-1) { + // Ceil + t.offset = t.cy - (height - 1) + } else if t.offset > t.cy { + // Floor + t.offset = t.cy + } + + // Adjustment + if count-t.offset < height { + t.offset = Max(0, count-height) + t.cy = Max(0, Min(t.offset+diffpos, count-1)) + } +} + +func (t *Terminal) vmove(o int) { + if t.reverse { + t.cy -= o + } else { + t.cy += o + } +} + +func maxItems() int { + return C.MaxY() - 2 +} diff --git a/src/tokenizer.go b/src/tokenizer.go new file mode 100644 index 0000000..c187529 --- /dev/null +++ b/src/tokenizer.go @@ -0,0 +1,194 @@ +package fzf + +import ( + "regexp" + "strconv" + "strings" +) + +const RANGE_ELLIPSIS = 0 + +type Range struct { + begin int + end int +} + +type Transformed struct { + whole *string + parts []Token +} + +type Token struct { + text *string + prefixLength int +} + +func ParseRange(str *string) (Range, bool) { + if (*str) == ".." { + return Range{RANGE_ELLIPSIS, RANGE_ELLIPSIS}, true + } else if strings.HasPrefix(*str, "..") { + end, err := strconv.Atoi((*str)[2:]) + if err != nil || end == 0 { + return Range{}, false + } else { + return Range{RANGE_ELLIPSIS, end}, true + } + } else if strings.HasSuffix(*str, "..") { + begin, err := strconv.Atoi((*str)[:len(*str)-2]) + if err != nil || begin == 0 { + return Range{}, false + } else { + return Range{begin, RANGE_ELLIPSIS}, true + } + } else if strings.Contains(*str, "..") { + ns := strings.Split(*str, "..") + if len(ns) != 2 { + return Range{}, false + } + begin, err1 := strconv.Atoi(ns[0]) + end, err2 := strconv.Atoi(ns[1]) + if err1 != nil || err2 != nil { + return Range{}, false + } + return Range{begin, end}, true + } + + n, err := strconv.Atoi(*str) + if err != nil || n == 0 { + return Range{}, false + } + return Range{n, n}, true +} + +func withPrefixLengths(tokens []string, begin int) []Token { + ret := make([]Token, len(tokens)) + + prefixLength := begin + for idx, token := range tokens { + // Need to define a new local variable instead of the reused token to take + // the pointer to it + str := token + ret[idx] = Token{text: &str, prefixLength: prefixLength} + prefixLength += len([]rune(token)) + } + return ret +} + +const ( + AWK_NIL = iota + AWK_BLACK + AWK_WHITE +) + +func awkTokenizer(input *string) ([]string, int) { + // 9, 32 + ret := []string{} + str := []rune{} + prefixLength := 0 + state := AWK_NIL + for _, r := range []rune(*input) { + white := r == 9 || r == 32 + switch state { + case AWK_NIL: + if white { + prefixLength++ + } else { + state = AWK_BLACK + str = append(str, r) + } + case AWK_BLACK: + str = append(str, r) + if white { + state = AWK_WHITE + } + case AWK_WHITE: + if white { + str = append(str, r) + } else { + ret = append(ret, string(str)) + state = AWK_BLACK + str = []rune{r} + } + } + } + if len(str) > 0 { + ret = append(ret, string(str)) + } + return ret, prefixLength +} + +func Tokenize(str *string, delimiter *regexp.Regexp) []Token { + prefixLength := 0 + if delimiter == nil { + // AWK-style (\S+\s*) + tokens, prefixLength := awkTokenizer(str) + return withPrefixLengths(tokens, prefixLength) + } else { + tokens := delimiter.FindAllString(*str, -1) + return withPrefixLengths(tokens, prefixLength) + } +} + +func joinTokens(tokens []Token) string { + ret := "" + for _, token := range tokens { + ret += *token.text + } + return ret +} + +func Transform(tokens []Token, withNth []Range) *Transformed { + transTokens := make([]Token, len(withNth)) + numTokens := len(tokens) + whole := "" + for idx, r := range withNth { + part := "" + minIdx := 0 + if r.begin == r.end { + idx := r.begin + if idx == RANGE_ELLIPSIS { + part += joinTokens(tokens) + } else { + if idx < 0 { + idx += numTokens + 1 + } + if idx >= 1 && idx <= numTokens { + minIdx = idx - 1 + part += *tokens[idx-1].text + } + } + } else { + var begin, end int + if r.begin == RANGE_ELLIPSIS { // ..N + begin, end = 1, r.end + if end < 0 { + end += numTokens + 1 + } + } else if r.end == RANGE_ELLIPSIS { // N.. + begin, end = r.begin, numTokens + if begin < 0 { + begin += numTokens + 1 + } + } else { + begin, end = r.begin, r.end + if begin < 0 { + begin += numTokens + 1 + } + if end < 0 { + end += numTokens + 1 + } + } + minIdx = Max(0, begin-1) + for idx := begin; idx <= end; idx++ { + if idx >= 1 && idx <= numTokens { + part += *tokens[idx-1].text + } + } + } + whole += part + transTokens[idx] = Token{&part, tokens[minIdx].prefixLength} + } + return &Transformed{ + whole: &whole, + parts: transTokens} +} diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go new file mode 100644 index 0000000..ed77efe --- /dev/null +++ b/src/tokenizer_test.go @@ -0,0 +1,97 @@ +package fzf + +import "testing" + +func TestParseRange(t *testing.T) { + { + i := ".." + r, _ := ParseRange(&i) + if r.begin != RANGE_ELLIPSIS || r.end != RANGE_ELLIPSIS { + t.Errorf("%s", r) + } + } + { + i := "3.." + r, _ := ParseRange(&i) + if r.begin != 3 || r.end != RANGE_ELLIPSIS { + t.Errorf("%s", r) + } + } + { + i := "3..5" + r, _ := ParseRange(&i) + if r.begin != 3 || r.end != 5 { + t.Errorf("%s", r) + } + } + { + i := "-3..-5" + r, _ := ParseRange(&i) + if r.begin != -3 || r.end != -5 { + t.Errorf("%s", r) + } + } + { + i := "3" + r, _ := ParseRange(&i) + if r.begin != 3 || r.end != 3 { + t.Errorf("%s", r) + } + } +} + +func TestTokenize(t *testing.T) { + // AWK-style + input := " abc: def: ghi " + tokens := Tokenize(&input, nil) + if *tokens[0].text != "abc: " || tokens[0].prefixLength != 2 { + t.Errorf("%s", tokens) + } + + // With delimiter + tokens = Tokenize(&input, delimiterRegexp(":")) + if *tokens[0].text != " abc:" || tokens[0].prefixLength != 0 { + t.Errorf("%s", tokens) + } +} + +func TestTransform(t *testing.T) { + input := " abc: def: ghi: jkl" + { + tokens := Tokenize(&input, nil) + { + ranges := splitNth("1,2,3") + tx := Transform(tokens, ranges) + if *tx.whole != "abc: def: ghi: " { + t.Errorf("%s", *tx) + } + } + { + ranges := splitNth("1..2,3,2..,1") + tx := Transform(tokens, ranges) + if *tx.whole != "abc: def: ghi: def: ghi: jklabc: " || + len(tx.parts) != 4 || + *tx.parts[0].text != "abc: def: " || tx.parts[0].prefixLength != 2 || + *tx.parts[1].text != "ghi: " || tx.parts[1].prefixLength != 14 || + *tx.parts[2].text != "def: ghi: jkl" || tx.parts[2].prefixLength != 8 || + *tx.parts[3].text != "abc: " || tx.parts[3].prefixLength != 2 { + t.Errorf("%s", *tx) + } + } + } + { + tokens := Tokenize(&input, delimiterRegexp(":")) + { + ranges := splitNth("1..2,3,2..,1") + tx := Transform(tokens, ranges) + if *tx.whole != " abc: def: ghi: def: ghi: jkl abc:" || + len(tx.parts) != 4 || + *tx.parts[0].text != " abc: def:" || tx.parts[0].prefixLength != 0 || + *tx.parts[1].text != " ghi:" || tx.parts[1].prefixLength != 12 || + *tx.parts[2].text != " def: ghi: jkl" || tx.parts[2].prefixLength != 6 || + *tx.parts[3].text != " abc:" || tx.parts[3].prefixLength != 0 { + t.Errorf("%s", *tx) + } + } + } +} diff --git a/src/util.go b/src/util.go new file mode 100644 index 0000000..2144e54 --- /dev/null +++ b/src/util.go @@ -0,0 +1,21 @@ +package fzf + +func Max(first int, items ...int) int { + max := first + for _, item := range items { + if item > max { + max = item + } + } + return max +} + +func Min(first int, items ...int) int { + min := first + for _, item := range items { + if item < min { + min = item + } + } + return min +} diff --git a/src/util_test.go b/src/util_test.go new file mode 100644 index 0000000..814b42c --- /dev/null +++ b/src/util_test.go @@ -0,0 +1,18 @@ +package fzf + +import "testing" + +func TestMax(t *testing.T) { + if Max(-2, 5, 1, 4, 3) != 5 { + t.Error("Invalid result") + } +} + +func TestMin(t *testing.T) { + if Min(2, -3) != -3 { + t.Error("Invalid result") + } + if Min(-2, 5, 1, 4, 3) != -2 { + t.Error("Invalid result") + } +} From baad26a0fd0d39640ec31f98d92a8745b022f755 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 01:36:33 +0900 Subject: [PATCH 002/408] Fix exit conditions of --select-1 and --exit-0 --- src/core.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core.go b/src/core.go index 2601397..4cdf79a 100644 --- a/src/core.go +++ b/src/core.go @@ -94,7 +94,8 @@ func Run(options *Options) { chunks: chunkList.Snapshot(), pattern: pattern}, limit) - if !cancelled && (filtering || opts.Exit0) { + if !cancelled && (filtering || + opts.Exit0 && len(matches) == 0 || opts.Select1 && len(matches) == 1) { if opts.PrintQuery { fmt.Println(patternString) } From 40d0a6347c65b523d60f8d7898eafc42e1e4a3b6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 01:47:59 +0900 Subject: [PATCH 003/408] Fix scan limit for --select-1 and --exit-0 options --- src/matcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matcher.go b/src/matcher.go index 363b07f..ad782bd 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -150,7 +150,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) ([]*Item, bool) { if cancelled.Get() { return } - countChan <- len(sliceMatches) + countChan <- len(matches) } if !empty && m.sort { sort.Sort(ByRelevance(sliceMatches)) From 9930a1d4d9cf92fe869f9352177dd24bdf1ac13f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 02:00:22 +0900 Subject: [PATCH 004/408] Update install script to download tarball --- install | 11 ++++++++--- src/Makefile | 7 +++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/install b/install index 6b64a3f..5a99930 100755 --- a/install +++ b/install @@ -6,16 +6,21 @@ fzf_base=`pwd` ARCHI=$(uname -sm) download() { + mkdir -p "$fzf_base"/bin + cd "$fzf_base"/bin echo "Downloading fzf executable ($1) ..." - if curl -fLo "$fzf_base"/bin/fzf https://github.com/junegunn/fzf-bin/releases/download/snapshot/$1; then - chmod +x "$fzf_base"/bin/fzf + if curl -fL \ + https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tar.gz | + tar -xz; then + mv $1 fzf + chmod +x fzf else echo "Failed to download $1" exit 1 fi + cd - > /dev/null } -mkdir -p "$fzf_base"/bin if [ "$ARCHI" = "Darwin x86_64" ]; then download fzf_darwin_amd64 elif [ "$ARCHI" = "Linux x86_64" ]; then diff --git a/src/Makefile b/src/Makefile index bae4c90..fecf7d2 100644 --- a/src/Makefile +++ b/src/Makefile @@ -25,7 +25,10 @@ SOURCES = $(wildcard *.go fzf/*.go) all: build -build: $(BINARY) +build: $(BINARY).tar.gz + +$(BINARY).tar.gz: $(BINARY) + cd fzf && tar -czf $(notdir $(BINARY)).tar.gz $(notdir $(BINARY)) $(BINARY): $(SOURCES) go get @@ -37,7 +40,7 @@ install: $(BINARY) cp -f $(BINARY) $(BINDIR)/fzf clean: - rm -f $(BINARY) + rm -f $(BINARY) $(BINARY).tar.gz docker: docker build -t junegunn/ubuntu-sandbox . From 0a6cb62169ebe1abb63aa5c70868df7c40441b1c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 02:42:58 +0900 Subject: [PATCH 005/408] Fall back to Ruby version when download failed --- install | 52 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/install b/install index 5a99930..46f6553 100755 --- a/install +++ b/install @@ -6,27 +6,49 @@ fzf_base=`pwd` ARCHI=$(uname -sm) download() { - mkdir -p "$fzf_base"/bin - cd "$fzf_base"/bin echo "Downloading fzf executable ($1) ..." - if curl -fL \ - https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tar.gz | - tar -xz; then - mv $1 fzf - chmod +x fzf - else - echo "Failed to download $1" - exit 1 + mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin + if [ $? -ne 0 ]; then + echo "- Failed to create bin directory." + return 1 fi - cd - > /dev/null + + local url=https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tar.gz + if which curl > /dev/null; then + curl -fL $url | tar -xz + elif which wget > /dev/null; then + wget -O - $url | tar -xz + else + echo "- curl or wget required to download fzf executable." + return 1 + fi + + if [ ! -f $1 ]; then + echo "- Failed to download ${1}." + return 1 + fi + + mv $1 fzf && chmod +x fzf && cd - > /dev/null && echo } +# Try to download binary executable +binary_available=0 +downloaded=0 if [ "$ARCHI" = "Darwin x86_64" ]; then - download fzf_darwin_amd64 + binary_available=1 + download fzf_darwin_amd64 && downloaded=1 elif [ "$ARCHI" = "Linux x86_64" ]; then - download fzf_linux_amd64 -else # No prebuilt executable - echo "No prebuilt binary for $ARCHI ... Installing legacy Ruby version ..." + binary_available=1 + download fzf_linux_amd64 && downloaded=1 +fi + +if [ $downloaded -ne 1 ]; then + if [ $binary_available -eq 0 ]; then + echo -n "No prebuilt binary for $ARCHI ... " + else + echo -n "Failed to download binary executable ... " + fi + echo "Installing legacy Ruby version ..." # ruby executable echo -n "Checking Ruby executable ... " From 0dd024a09fc4dd37a596a07e7ff0043537895909 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 05:00:28 +0900 Subject: [PATCH 006/408] Remove unnecessary delay on non/defered interactive mode --- src/core.go | 3 ++- src/eventbox.go | 26 ++++++++++++++++++++-- src/eventbox_test.go | 51 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/eventbox_test.go diff --git a/src/core.go b/src/core.go index 4cdf79a..7abee80 100644 --- a/src/core.go +++ b/src/core.go @@ -77,6 +77,7 @@ func Run(options *Options) { pattern := patternBuilder([]rune(patternString)) looping := true + eventBox.Unwatch(EVT_READ_NEW) for looping { eventBox.Wait(func(events *Events) { for evt, _ := range *events { @@ -87,7 +88,6 @@ func Run(options *Options) { } } }) - time.Sleep(COORDINATOR_DELAY) } matches, cancelled := matcher.scan(MatchRequest{ @@ -116,6 +116,7 @@ func Run(options *Options) { // Event coordination reading := true ticks := 0 + eventBox.Watch(EVT_READ_NEW) for { delay := true ticks += 1 diff --git a/src/eventbox.go b/src/eventbox.go index 6685e7c..95126cc 100644 --- a/src/eventbox.go +++ b/src/eventbox.go @@ -9,10 +9,14 @@ type Events map[EventType]interface{} type EventBox struct { events Events cond *sync.Cond + ignore map[EventType]bool } func NewEventBox() *EventBox { - return &EventBox{make(Events), sync.NewCond(&sync.Mutex{})} + return &EventBox{ + events: make(Events), + cond: sync.NewCond(&sync.Mutex{}), + ignore: make(map[EventType]bool)} } func (b *EventBox) Wait(callback func(*Events)) { @@ -30,7 +34,9 @@ func (b *EventBox) Set(event EventType, value interface{}) { b.cond.L.Lock() defer b.cond.L.Unlock() b.events[event] = value - b.cond.Broadcast() + if _, found := b.ignore[event]; !found { + b.cond.Broadcast() + } } // Unsynchronized; should be called within Wait routine @@ -46,3 +52,19 @@ func (b *EventBox) Peak(event EventType) bool { _, ok := b.events[event] return ok } + +func (b *EventBox) Watch(events ...EventType) { + b.cond.L.Lock() + defer b.cond.L.Unlock() + for _, event := range events { + delete(b.ignore, event) + } +} + +func (b *EventBox) Unwatch(events ...EventType) { + b.cond.L.Lock() + defer b.cond.L.Unlock() + for _, event := range events { + b.ignore[event] = true + } +} diff --git a/src/eventbox_test.go b/src/eventbox_test.go new file mode 100644 index 0000000..fb0ceed --- /dev/null +++ b/src/eventbox_test.go @@ -0,0 +1,51 @@ +package fzf + +import "testing" + +func TestEventBox(t *testing.T) { + eb := NewEventBox() + + // Wait should return immediately + ch := make(chan bool) + + go func() { + eb.Set(EVT_READ_NEW, 10) + ch <- true + <-ch + eb.Set(EVT_SEARCH_NEW, 10) + eb.Set(EVT_SEARCH_NEW, 15) + eb.Set(EVT_SEARCH_NEW, 20) + eb.Set(EVT_SEARCH_PROGRESS, 30) + ch <- true + <-ch + eb.Set(EVT_SEARCH_FIN, 40) + ch <- true + <-ch + }() + + count := 0 + sum := 0 + looping := true + for looping { + <-ch + eb.Wait(func(events *Events) { + for _, value := range *events { + switch val := value.(type) { + case int: + sum += val + looping = sum < 100 + } + } + events.Clear() + }) + ch <- true + count += 1 + } + + if count != 3 { + t.Error("Invalid number of events", count) + } + if sum != 100 { + t.Error("Invalid sum", sum) + } +} From d2f7acbc69de26084c83bb07e2a175e05dce2fc2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 05:01:13 +0900 Subject: [PATCH 007/408] Remove race conditions when accessing the last chunk --- src/chunklist.go | 25 +++++++++++++++---------- src/chunklist_test.go | 20 ++++++++++++++------ src/core.go | 11 +++++++---- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/chunklist.go b/src/chunklist.go index b1f9638..5bca6da 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -42,14 +42,6 @@ func CountItems(cs []*Chunk) int { return CHUNK_SIZE*(len(cs)-1) + len(*(cs[len(cs)-1])) } -func (cl *ChunkList) Count() int { - return cl.count -} - -func (cl *ChunkList) Chunks() []*Chunk { - return cl.chunks -} - func (cl *ChunkList) Push(data string) { cl.mutex.Lock() defer cl.mutex.Unlock() @@ -63,11 +55,24 @@ func (cl *ChunkList) Push(data string) { cl.count += 1 } -func (cl *ChunkList) Snapshot() []*Chunk { +func (cl *ChunkList) Snapshot() ([]*Chunk, int) { cl.mutex.Lock() defer cl.mutex.Unlock() ret := make([]*Chunk, len(cl.chunks)) copy(ret, cl.chunks) - return ret + + // Duplicate the last chunk + if cnt := len(ret); cnt > 0 { + ret[cnt-1] = ret[cnt-1].dupe() + } + return ret, cl.count +} + +func (c *Chunk) dupe() *Chunk { + newChunk := make(Chunk, len(*c)) + for idx, ptr := range *c { + newChunk[idx] = ptr + } + return &newChunk } diff --git a/src/chunklist_test.go b/src/chunklist_test.go index a7daa47..b244ece 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -11,8 +11,8 @@ func TestChunkList(t *testing.T) { }) // Snapshot - snapshot := cl.Snapshot() - if len(snapshot) > 0 { + snapshot, count := cl.Snapshot() + if len(snapshot) > 0 || count > 0 { t.Error("Snapshot should be empty now") } @@ -26,8 +26,8 @@ func TestChunkList(t *testing.T) { } // But the new snapshot should contain the added items - snapshot = cl.Snapshot() - if len(snapshot) != 1 { + snapshot, count = cl.Snapshot() + if len(snapshot) != 1 && count != 2 { t.Error("Snapshot should not be empty now") } @@ -55,12 +55,20 @@ func TestChunkList(t *testing.T) { } // New snapshot - snapshot = cl.Snapshot() + snapshot, count = cl.Snapshot() if len(snapshot) != 3 || !snapshot[0].IsFull() || - !snapshot[1].IsFull() || snapshot[2].IsFull() { + !snapshot[1].IsFull() || snapshot[2].IsFull() || count != CHUNK_SIZE*2+2 { t.Error("Expected two full chunks and one more chunk") } if len(*snapshot[2]) != 2 { t.Error("Unexpected number of items") } + + cl.Push("hello") + cl.Push("world") + + lastChunkCount := len(*snapshot[len(snapshot)-1]) + if lastChunkCount != 2 { + t.Error("Unexpected number of items:", lastChunkCount) + } } diff --git a/src/core.go b/src/core.go index 7abee80..b6f0857 100644 --- a/src/core.go +++ b/src/core.go @@ -90,8 +90,9 @@ func Run(options *Options) { }) } + snapshot, _ := chunkList.Snapshot() matches, cancelled := matcher.scan(MatchRequest{ - chunks: chunkList.Snapshot(), + chunks: snapshot, pattern: pattern}, limit) if !cancelled && (filtering || @@ -127,11 +128,13 @@ func Run(options *Options) { case EVT_READ_NEW, EVT_READ_FIN: reading = reading && evt == EVT_READ_NEW - terminal.UpdateCount(chunkList.Count(), !reading) - matcher.Reset(chunkList.Snapshot(), terminal.Input(), false) + snapshot, count := chunkList.Snapshot() + terminal.UpdateCount(count, !reading) + matcher.Reset(snapshot, terminal.Input(), false) case EVT_SEARCH_NEW: - matcher.Reset(chunkList.Snapshot(), terminal.Input(), true) + snapshot, _ := chunkList.Snapshot() + matcher.Reset(snapshot, terminal.Input(), true) delay = false case EVT_SEARCH_PROGRESS: From 606d33e77e6e6aa2f03c4886db781260caff3a34 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 05:09:40 +0900 Subject: [PATCH 008/408] Remove race conditions from screen update --- src/terminal.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index b6c7154..4d5c9cc 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -297,7 +297,6 @@ func (t *Terminal) printAll() { } func (t *Terminal) refresh() { - t.placeCursor() C.Refresh() } @@ -353,6 +352,7 @@ func (t *Terminal) Loop() { t.initFunc() t.printInfo() t.printPrompt() + t.placeCursor() t.refresh() t.mutex.Unlock() } @@ -382,6 +382,7 @@ func (t *Terminal) Loop() { os.Exit(1) } } + t.placeCursor() t.mutex.Unlock() }) t.refresh() From f9f9b671c5dda3a88b515e9a5e9f9cbf292b849b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 14:29:42 +0900 Subject: [PATCH 009/408] Ask if fzf executable already exists --- install | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/install b/install index 46f6553..8ea1435 100755 --- a/install +++ b/install @@ -5,8 +5,17 @@ fzf_base=`pwd` ARCHI=$(uname -sm) +ask() { + read -p "$1 ([y]/n) " -n 1 -r + echo + [[ ! $REPLY =~ ^[Nn]$ ]] +} + download() { echo "Downloading fzf executable ($1) ..." + if [ -x "$fzf_base"/bin/fzf ]; then + ask "- fzf already exists. Download it again?" || return 0 + fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin if [ $? -ne 0 ]; then echo "- Failed to create bin directory." @@ -108,15 +117,11 @@ if [ $downloaded -ne 1 ]; then fi # Auto-completion -read -p "Do you want to add auto-completion support? ([y]/n) " -n 1 -r -echo -[[ ! $REPLY =~ ^[Nn]$ ]] +ask "Do you want to add auto-completion support?" auto_completion=$? # Key-bindings -read -p "Do you want to add key bindings? ([y]/n) " -n 1 -r -echo -[[ ! $REPLY =~ ^[Nn]$ ]] +ask "Do you want to add key bindings?" key_bindings=$? echo From 53bce0581edeac68b49af7608cfc080d52ab5cc3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jan 2015 14:35:13 +0900 Subject: [PATCH 010/408] Update fish function --- install | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/install b/install index 8ea1435..3a708a1 100755 --- a/install +++ b/install @@ -303,11 +303,19 @@ if [ -n "$(which fish)" ]; then has_fish=1 echo -n "Generate ~/.config/fish/functions/fzf.fish ... " mkdir -p ~/.config/fish/functions - cat > ~/.config/fish/functions/fzf.fish << EOFZF + if [ $downloaded -eq 0 ]; then + cat > ~/.config/fish/functions/fzf.fish << EOFZF function fzf $fzf_cmd \$argv end EOFZF + else + cat > ~/.config/fish/functions/fzf.fish << EOFZF +function fzf + $fzf_base/bin/fzf \$argv +end +EOFZF + fi echo "OK" if [ $key_bindings -eq 0 ]; then From 755773756950ae3124eb82224c21a42e605b6194 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 00:52:08 +0900 Subject: [PATCH 011/408] Remove outdated information from README --- README.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/README.md b/README.md index 0b52864..998db0c 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,6 @@ fzf is a general-purpose fuzzy finder for your shell. It was heavily inspired by [ctrlp.vim](https://github.com/kien/ctrlp.vim) and the likes. -Requirements ------------- - -fzf requires Ruby (>= 1.8.5). - Installation ------------ @@ -436,21 +431,6 @@ If you have any rendering issues, check the followings: option. And if it solves your problem, I recommend including it in `FZF_DEFAULT_OPTS` for further convenience. 4. If you still have problem, try `--no-256` option or even `--no-color`. -5. Ruby 1.9 or above is required for correctly displaying unicode characters. - -### Ranking algorithm - -fzf sorts the result first by the length of the matched substring, then by the -length of the whole string. However it only does so when the number of matches -is less than the limit which is by default 1000, in order to avoid the cost of -sorting a large list and limit the response time of the query. - -This limit can be adjusted with `-s` option, or with the environment variable -`FZF_DEFAULT_OPTS`. - -```sh -export FZF_DEFAULT_OPTS="--sort 20000" -``` ### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` @@ -545,12 +525,6 @@ function fe end ``` -### Windows - -fzf works on [Cygwin](http://www.cygwin.com/) and -[MSYS2](http://sourceforge.net/projects/msys2/). You may need to use `--black` -option on MSYS2 to avoid rendering issues. - ### Handling UTF-8 NFD paths on OSX Use iconv to convert NFD paths to NFC: From 8e5ecf6b383c35c7f33f7933e35959c2fc9b893c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 01:25:54 +0900 Subject: [PATCH 012/408] Update Makefile and installer to use version number --- install | 17 ++++++++--------- src/Makefile | 45 ++++++++++++++++++++++----------------------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/install b/install index 3a708a1..e331d31 100755 --- a/install +++ b/install @@ -3,8 +3,6 @@ cd `dirname $BASH_SOURCE` fzf_base=`pwd` -ARCHI=$(uname -sm) - ask() { read -p "$1 ([y]/n) " -n 1 -r echo @@ -22,7 +20,7 @@ download() { return 1 fi - local url=https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tar.gz + local url=https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tgz if which curl > /dev/null; then curl -fL $url | tar -xz elif which wget > /dev/null; then @@ -41,19 +39,20 @@ download() { } # Try to download binary executable -binary_available=0 +archi=$(uname -sm) downloaded=0 -if [ "$ARCHI" = "Darwin x86_64" ]; then +binary_available=0 +if [ "$archi" = "Darwin x86_64" ]; then binary_available=1 - download fzf_darwin_amd64 && downloaded=1 -elif [ "$ARCHI" = "Linux x86_64" ]; then + download fzf-0.9.0-darwin_amd64 && downloaded=1 +elif [ "$archi" = "Linux x86_64" ]; then binary_available=1 - download fzf_linux_amd64 && downloaded=1 + download fzf-0.9.0-linux_amd64 && downloaded=1 fi if [ $downloaded -ne 1 ]; then if [ $binary_available -eq 0 ]; then - echo -n "No prebuilt binary for $ARCHI ... " + echo -n "No prebuilt binary for $archi ... " else echo -n "Failed to download binary executable ... " fi diff --git a/src/Makefile b/src/Makefile index fecf7d2..10429a1 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,52 +1,51 @@ -BINARY := fzf/fzf - UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) - BINARY := $(BINARY)_darwin + SUFFIX := darwin else ifeq ($(UNAME_S),Linux) - BINARY := $(BINARY)_linux + SUFFIX := linux endif UNAME_M := $(shell uname -m) -ifneq ($(filter i386 i686,$(UNAME_M)),) -$(error "filtered is not supported, yet.") -endif - ifeq ($(UNAME_M),x86_64) - BINARY := $(BINARY)_amd64 + SUFFIX := $(SUFFIX)_amd64 else ifneq ($(filter i386 i686,$(UNAME_M)),) - BINARY := $(BINARY)_386 + SUFFIX := $(SUFFIX)_386 else # TODO $(error "$(UNAME_M) is not supported, yet.") endif -BINDIR = ../bin -SOURCES = $(wildcard *.go fzf/*.go) +BINARY := fzf-$(SUFFIX) +BINDIR := ../bin +SOURCES := $(wildcard *.go fzf/*.go) +RELEASE = fzf-$(shell fzf/$(BINARY) --version)-$(SUFFIX) -all: build +all: release -build: $(BINARY).tar.gz +release: build + cd fzf && \ + cp $(BINARY) $(RELEASE) && \ + tar -czf $(RELEASE).tgz $(RELEASE) && \ + rm $(RELEASE) -$(BINARY).tar.gz: $(BINARY) - cd fzf && tar -czf $(notdir $(BINARY)).tar.gz $(notdir $(BINARY)) +build: fzf/$(BINARY) -$(BINARY): $(SOURCES) +fzf/$(BINARY): $(SOURCES) go get go test -v - cd fzf && go build -o $(notdir $(BINARY)) + cd fzf && go build -o $(BINARY) -install: $(BINARY) +install: fzf/$(BINARY) mkdir -p $(BINDIR) - cp -f $(BINARY) $(BINDIR)/fzf + cp -f fzf/$(BINARY) $(BINDIR)/fzf clean: - rm -f $(BINARY) $(BINARY).tar.gz + cd fzf && rm -f $(BINARY) $(RELEASE).tgz docker: docker build -t junegunn/ubuntu-sandbox . linux64: docker run -i -t -u jg -v $(shell cd ..; pwd):/fzf junegunn/ubuntu-sandbox \ - /bin/bash -ci 'cd ~jg/go/src/github.com/junegunn/fzf/src; make build' + /bin/bash -ci 'cd ~jg/go/src/github.com/junegunn/fzf/src; make' -.PHONY: build install linux64 clean docker run +.PHONY: build release install linux64 clean docker run From dee0909d2bd665a885855c366ed2b2137819d8fb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 01:40:19 +0900 Subject: [PATCH 013/408] Fix mouse click offset when list is scrolled --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 4d5c9cc..28a9a33 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -526,7 +526,7 @@ func (t *Terminal) Loop() { req(REQ_PROMPT) } else if my >= 2 { // List - t.cy = my - 2 + t.cy = t.offset + my - 2 if me.Mod { toggle() } From ea25e9674f84071dab194a6e35973373ef03e02a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 02:17:26 +0900 Subject: [PATCH 014/408] Refactor install script --- install | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/install b/install index e331d31..855e056 100755 --- a/install +++ b/install @@ -1,5 +1,7 @@ #!/usr/bin/env bash +version=0.9.0 + cd `dirname $BASH_SOURCE` fzf_base=`pwd` @@ -44,10 +46,10 @@ downloaded=0 binary_available=0 if [ "$archi" = "Darwin x86_64" ]; then binary_available=1 - download fzf-0.9.0-darwin_amd64 && downloaded=1 + download fzf-$version-darwin_amd64 && downloaded=1 elif [ "$archi" = "Linux x86_64" ]; then binary_available=1 - download fzf-0.9.0-linux_amd64 && downloaded=1 + download fzf-$version-linux_amd64 && downloaded=1 fi if [ $downloaded -ne 1 ]; then From 4a5142c60b1e833425a19de744b48ad1753f0543 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 02:32:18 +0900 Subject: [PATCH 015/408] Do not sort terms when building cache key --- src/pattern.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 533aa59..7c27f52 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -2,7 +2,6 @@ package fzf import ( "regexp" - "sort" "strings" ) @@ -181,7 +180,6 @@ func (p *Pattern) CacheKey() string { } cacheableTerms = append(cacheableTerms, string(term.origText)) } - sort.Strings(cacheableTerms) return strings.Join(cacheableTerms, " ") } From 82156d34ccf109b95b626852741bee8ee74f8378 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 12:21:26 +0900 Subject: [PATCH 016/408] Update Makefile and install script fzf may not run correctly on some OS even when the binary the platform is successfully downloaded. The install script is updated to check if the system has no problem running the executable and fall back to Ruby version when necessary. --- install | 52 ++++++++++++++++++++++++++++++++-------------------- src/Makefile | 4 ++-- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/install b/install index 855e056..b454715 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ version=0.9.0 -cd `dirname $BASH_SOURCE` -fzf_base=`pwd` +cd $(dirname $BASH_SOURCE) +fzf_base=$(pwd) ask() { read -p "$1 ([y]/n) " -n 1 -r @@ -11,15 +11,26 @@ ask() { [[ ! $REPLY =~ ^[Nn]$ ]] } +check_binary() { + echo "- Checking fzf executable" + echo -n " - " + if ! "$fzf_base"/bin/fzf --version; then + binary_error="Error occurred" + fi +} + download() { echo "Downloading fzf executable ($1) ..." if [ -x "$fzf_base"/bin/fzf ]; then - ask "- fzf already exists. Download it again?" || return 0 + if ! ask "- fzf already exists. Download it again?"; then + check_binary + return + fi fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin if [ $? -ne 0 ]; then - echo "- Failed to create bin directory." - return 1 + binary_error="Failed to create bin directory" + return fi local url=https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tgz @@ -28,35 +39,36 @@ download() { elif which wget > /dev/null; then wget -O - $url | tar -xz else - echo "- curl or wget required to download fzf executable." - return 1 + binary_error="curl or wget not found" + return fi if [ ! -f $1 ]; then - echo "- Failed to download ${1}." - return 1 + binary_error="Failed to download ${1}" + return fi - mv $1 fzf && chmod +x fzf && cd - > /dev/null && echo + mv $1 fzf && chmod +x fzf && check_binary } # Try to download binary executable archi=$(uname -sm) -downloaded=0 -binary_available=0 +binary_available=1 +binary_error="" if [ "$archi" = "Darwin x86_64" ]; then - binary_available=1 - download fzf-$version-darwin_amd64 && downloaded=1 + download fzf-$version-darwin_amd64 elif [ "$archi" = "Linux x86_64" ]; then - binary_available=1 - download fzf-$version-linux_amd64 && downloaded=1 + download fzf-$version-linux_amd64 +else + binary_available=0 fi -if [ $downloaded -ne 1 ]; then +cd "$fzf_base" +if [ -n "$binary_error" ]; then if [ $binary_available -eq 0 ]; then - echo -n "No prebuilt binary for $archi ... " + echo "No prebuilt binary for $archi ... " else - echo -n "Failed to download binary executable ... " + echo " - $binary_error ... " fi echo "Installing legacy Ruby version ..." @@ -304,7 +316,7 @@ if [ -n "$(which fish)" ]; then has_fish=1 echo -n "Generate ~/.config/fish/functions/fzf.fish ... " mkdir -p ~/.config/fish/functions - if [ $downloaded -eq 0 ]; then + if [ -n "$binary_error" ]; then cat > ~/.config/fish/functions/fzf.fish << EOFZF function fzf $fzf_cmd \$argv diff --git a/src/Makefile b/src/Makefile index 10429a1..a7235bc 100644 --- a/src/Makefile +++ b/src/Makefile @@ -44,8 +44,8 @@ clean: docker: docker build -t junegunn/ubuntu-sandbox . -linux64: +linux64: docker docker run -i -t -u jg -v $(shell cd ..; pwd):/fzf junegunn/ubuntu-sandbox \ /bin/bash -ci 'cd ~jg/go/src/github.com/junegunn/fzf/src; make' -.PHONY: build release install linux64 clean docker run +.PHONY: build release install linux64 clean docker From b42dcdb7a747cd5c7a412ca1dc8b7eb73b64f084 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 12:21:56 +0900 Subject: [PATCH 017/408] Update README for Go - System requirements --- src/README.md | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/README.md b/src/README.md index 2f3ca3b..7c47759 100644 --- a/src/README.md +++ b/src/README.md @@ -1,8 +1,9 @@ fzf in Go ========= -This directory contains the source code for the new fzf implementation in Go. -This new version has the following benefits over the previous Ruby version. +This directory contains the source code for the new fzf implementation in +[Go][go]. This new version has the following benefits over the previous Ruby +version. - Immensely faster - No GIL. Performance is linearly proportional to the number of cores. @@ -25,17 +26,28 @@ make install make linux64 ``` +System requirements +------------------- -Prebuilt binaries ------------------ +Currently prebuilt binaries are provided only for 64 bit OS X and Linux. +The install script will fall back to the legacy Ruby version on the other +systems, but if you have Go installed, you can try building it yourself. +(`make install`) -- Darwin x86_64 -- Linux x86_64 +However, as pointed out in [golang.org/doc/install][req], the Go version will +not run on CentOS/RHEL 5.x and thus the install script will choose the Ruby +version instead. + +The Go version depends on [ncurses][ncurses] and some Unix system calls, so it +shouldn't run natively on Windows at the moment. But it should be not +impossible to support Windows by falling back to a cross-platform alternative +such as [termbox][termbox] only on Windows. If you're interested in making fzf +work on Windows, please let me know. Third-party libraries used -------------------------- -- [ncurses](https://www.gnu.org/software/ncurses/) +- [ncurses][ncurses] - [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - Licensed under [MIT](http://mattn.mit-license.org/2013) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords) @@ -56,4 +68,9 @@ still don't have a quantitative measure of the performance. License ------- -- [MIT](LICENSE) +[MIT](LICENSE) + +[go]: https://golang.org/ +[ncurses]: https://www.gnu.org/software/ncurses/ +[req]: http://golang.org/doc/install +[termbox]: https://github.com/nsf/termbox-go From ee2ee025993421b243ef668e4d4ee395a5201820 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jan 2015 19:32:44 +0900 Subject: [PATCH 018/408] Fix index out of bounds error during Transform --- src/tokenizer.go | 8 +++++++- src/tokenizer_test.go | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/tokenizer.go b/src/tokenizer.go index c187529..bc1ca3a 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -186,7 +186,13 @@ func Transform(tokens []Token, withNth []Range) *Transformed { } } whole += part - transTokens[idx] = Token{&part, tokens[minIdx].prefixLength} + var prefixLength int + if minIdx < numTokens { + prefixLength = tokens[minIdx].prefixLength + } else { + prefixLength = 0 + } + transTokens[idx] = Token{&part, prefixLength} } return &Transformed{ whole: &whole, diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index ed77efe..1ae0c7e 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -95,3 +95,7 @@ func TestTransform(t *testing.T) { } } } + +func TestTransformIndexOutOfBounds(t *testing.T) { + Transform([]Token{}, splitNth("1")) +} From 3e6c950e12c5cdaa0a5e17915fc75ccd6e3648c2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 6 Jan 2015 02:04:06 +0900 Subject: [PATCH 019/408] Build i386 binary as well --- install | 14 ++++----- src/Dockerfile | 33 -------------------- src/Dockerfile.arch | 25 +++++++++++++++ src/Dockerfile.centos | 25 +++++++++++++++ src/Dockerfile.ubuntu | 30 ++++++++++++++++++ src/Makefile | 71 +++++++++++++++++++++++++++---------------- src/README.md | 4 +-- 7 files changed, 133 insertions(+), 69 deletions(-) delete mode 100644 src/Dockerfile create mode 100644 src/Dockerfile.arch create mode 100644 src/Dockerfile.centos create mode 100644 src/Dockerfile.ubuntu diff --git a/install b/install index b454715..b83920f 100755 --- a/install +++ b/install @@ -55,13 +55,13 @@ download() { archi=$(uname -sm) binary_available=1 binary_error="" -if [ "$archi" = "Darwin x86_64" ]; then - download fzf-$version-darwin_amd64 -elif [ "$archi" = "Linux x86_64" ]; then - download fzf-$version-linux_amd64 -else - binary_available=0 -fi +case "$archi" in + "Darwin x86_64") download fzf-$version-darwin_amd64 ;; +# "Darwin i[36]86") download fzf-$version-darwin_386 ;; + "Linux x86_64") download fzf-$version-linux_amd64 ;; +# "Linux i[36]86") download fzf-$version-linux_386 ;; + *) binary_available=0 ;; +esac cd "$fzf_base" if [ -n "$binary_error" ]; then diff --git a/src/Dockerfile b/src/Dockerfile deleted file mode 100644 index 3c062ee..0000000 --- a/src/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM ubuntu:14.04 -MAINTAINER Junegunn Choi - -# apt-get -RUN apt-get update && apt-get -y upgrade -RUN apt-get install -y --force-yes git vim-nox curl procps sudo \ - build-essential libncurses-dev - -# Setup jg user with sudo privilege -RUN useradd -s /bin/bash -m jg && echo 'jg:jg' | chpasswd && \ - echo 'jg ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/jg - -# Setup dotfiles -USER jg -RUN cd ~ && git clone https://github.com/junegunn/dotfiles.git && \ - dotfiles/install > /dev/null - -# Install Go 1.4 -RUN cd ~ && curl https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | tar -xz && \ - mv go go1.4 && \ - echo 'export GOROOT=~/go1.4' >> ~/dotfiles/bashrc-extra && \ - echo 'export PATH=~/go1.4/bin:$PATH' >> ~/dotfiles/bashrc-extra - -# Symlink fzf directory -RUN mkdir -p ~jg/go/src/github.com/junegunn && \ - ln -s /fzf ~jg/go/src/github.com/junegunn/fzf - -# Volume -VOLUME /fzf - -# Default CMD -CMD cd ~jg/go/src/github.com/junegunn/fzf/src && /bin/bash -l - diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch new file mode 100644 index 0000000..9fa4ea3 --- /dev/null +++ b/src/Dockerfile.arch @@ -0,0 +1,25 @@ +FROM base/archlinux:2014.07.03 +MAINTAINER Junegunn Choi + +# apt-get +RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git + +# Install Go 1.4 +RUN cd / && curl \ + https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ + tar -xz && mv go go1.4 + +ENV GOPATH /go +ENV GOROOT /go1.4 +ENV PATH /go1.4/bin:$PATH + +# Symlink fzf directory +RUN mkdir -p /go/src/github.com/junegunn && \ + ln -s /fzf /go/src/github.com/junegunn/fzf + +# Volume +VOLUME /fzf + +# Default CMD +CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash + diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos new file mode 100644 index 0000000..e791dc6 --- /dev/null +++ b/src/Dockerfile.centos @@ -0,0 +1,25 @@ +FROM centos:centos7 +MAINTAINER Junegunn Choi + +# yum +RUN yum install -y git gcc make tar ncurses-devel + +# Install Go 1.4 +RUN cd / && curl \ + https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ + tar -xz && mv go go1.4 + +ENV GOPATH /go +ENV GOROOT /go1.4 +ENV PATH /go1.4/bin:$PATH + +# Symlink fzf directory +RUN mkdir -p /go/src/github.com/junegunn && \ + ln -s /fzf /go/src/github.com/junegunn/fzf + +# Volume +VOLUME /fzf + +# Default CMD +CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash + diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu new file mode 100644 index 0000000..b7f6232 --- /dev/null +++ b/src/Dockerfile.ubuntu @@ -0,0 +1,30 @@ +FROM ubuntu:14.04 +MAINTAINER Junegunn Choi + +# apt-get +RUN apt-get update && apt-get -y upgrade && \ + apt-get install -y --force-yes git curl build-essential libncurses-dev + +# Install Go 1.4 +RUN cd / && curl \ + https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ + tar -xz && mv go go1.4 + +ENV GOPATH /go +ENV GOROOT /go1.4 +ENV PATH /go1.4/bin:$PATH + +# For i386 build +RUN apt-get install -y lib32ncurses5-dev && \ + cd $GOROOT/src && GOARCH=386 ./make.bash + +# Symlink fzf directory +RUN mkdir -p /go/src/github.com/junegunn && \ + ln -s /fzf /go/src/github.com/junegunn/fzf + +# Volume +VOLUME /fzf + +# Default CMD +CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash + diff --git a/src/Makefile b/src/Makefile index a7235bc..037fb61 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,51 +1,68 @@ UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) - SUFFIX := darwin + GOOS := darwin else ifeq ($(UNAME_S),Linux) - SUFFIX := linux + GOOS := linux endif -UNAME_M := $(shell uname -m) -ifeq ($(UNAME_M),x86_64) - SUFFIX := $(SUFFIX)_amd64 -else ifneq ($(filter i386 i686,$(UNAME_M)),) - SUFFIX := $(SUFFIX)_386 -else # TODO -$(error "$(UNAME_M) is not supported, yet.") +ifneq ($(shell uname -m),x86_64) +$(error "Build on $(UNAME_M) is not supported, yet.") endif -BINARY := fzf-$(SUFFIX) -BINDIR := ../bin -SOURCES := $(wildcard *.go fzf/*.go) -RELEASE = fzf-$(shell fzf/$(BINARY) --version)-$(SUFFIX) +SOURCES := $(wildcard *.go fzf/*.go) +BINDIR := ../bin + +BINARY32 := fzf-$(GOOS)_386 +BINARY64 := fzf-$(GOOS)_amd64 +RELEASE32 = fzf-$(shell fzf/$(BINARY64) --version)-$(GOOS)_386 +RELEASE64 = fzf-$(shell fzf/$(BINARY64) --version)-$(GOOS)_amd64 all: release release: build cd fzf && \ - cp $(BINARY) $(RELEASE) && \ - tar -czf $(RELEASE).tgz $(RELEASE) && \ - rm $(RELEASE) + cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \ + cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ + rm $(RELEASE32) $(RELEASE64) -build: fzf/$(BINARY) +build: fzf/$(BINARY32) fzf/$(BINARY64) -fzf/$(BINARY): $(SOURCES) +test: $(SOURCES) go get go test -v - cd fzf && go build -o $(BINARY) -install: fzf/$(BINARY) +fzf/$(BINARY32): test + cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32) + +fzf/$(BINARY64): test + cd fzf && go build -o $(BINARY64) + +install: fzf/$(BINARY64) mkdir -p $(BINDIR) - cp -f fzf/$(BINARY) $(BINDIR)/fzf + cp -f fzf/$(BINARY64) $(BINDIR)/fzf clean: - cd fzf && rm -f $(BINARY) $(RELEASE).tgz + cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz + +DISTRO := ubuntu docker: - docker build -t junegunn/ubuntu-sandbox . + docker build -t junegunn/$(DISTRO)-sandbox - < Dockerfile.$(DISTRO) -linux64: docker - docker run -i -t -u jg -v $(shell cd ..; pwd):/fzf junegunn/ubuntu-sandbox \ - /bin/bash -ci 'cd ~jg/go/src/github.com/junegunn/fzf/src; make' +linux: docker + docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/$(DISTRO)-sandbox \ + /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make' -.PHONY: build release install linux64 clean docker +$(DISTRO): docker + docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/$(DISTRO)-sandbox \ + sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + +arch: docker + docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/arch-sandbox \ + sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + +centos: docker + docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/centos-sandbox \ + sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + +.PHONY: build release install linux clean docker $(DISTRO) diff --git a/src/README.md b/src/README.md index 7c47759..70b2c1d 100644 --- a/src/README.md +++ b/src/README.md @@ -22,8 +22,8 @@ make # Install the executable to ../bin directory make install -# Build executable for Linux x86_64 using Docker -make linux64 +# Build executables for Linux using Docker +make linux ``` System requirements From 383f908cf79ed8e97e7728db8ec2e0ab7e270bc3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 6 Jan 2015 02:04:27 +0900 Subject: [PATCH 020/408] Remove unnecessary event dispatch --- src/terminal.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 28a9a33..a935bd3 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -523,7 +523,6 @@ func (t *Terminal) Loop() { if my == 0 && mx >= 0 { // Prompt t.cx = mx - req(REQ_PROMPT) } else if my >= 2 { // List t.cy = t.offset + my - 2 From 6109a0fe4442007cb3cd3df53730f74418ceaf37 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 6 Jan 2015 02:07:30 +0900 Subject: [PATCH 021/408] Refactor Makefile --- src/Makefile | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Makefile b/src/Makefile index 037fb61..3e57a17 100644 --- a/src/Makefile +++ b/src/Makefile @@ -44,6 +44,7 @@ install: fzf/$(BINARY64) clean: cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz +# Linux distribution to build fzf on DISTRO := ubuntu docker: @@ -57,12 +58,4 @@ $(DISTRO): docker docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/$(DISTRO)-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' -arch: docker - docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/arch-sandbox \ - sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' - -centos: docker - docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/centos-sandbox \ - sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' - .PHONY: build release install linux clean docker $(DISTRO) From b277f5ae6fe9b263410945796263aa52d95d4ab5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 7 Jan 2015 00:24:05 +0900 Subject: [PATCH 022/408] Fix i386 build --- install | 13 +++++++------ src/Dockerfile.arch | 6 ++++++ src/Makefile | 2 +- src/README.md | 7 +++---- src/curses/curses.go | 23 ++++++++++++----------- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/install b/install index b83920f..ce5ec79 100755 --- a/install +++ b/install @@ -15,6 +15,7 @@ check_binary() { echo "- Checking fzf executable" echo -n " - " if ! "$fzf_base"/bin/fzf --version; then + rm -v "$fzf_base"/bin/fzf binary_error="Error occurred" fi } @@ -33,7 +34,7 @@ download() { return fi - local url=https://github.com/junegunn/fzf-bin/releases/download/snapshot/${1}.tgz + local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz if which curl > /dev/null; then curl -fL $url | tar -xz elif which wget > /dev/null; then @@ -56,11 +57,11 @@ archi=$(uname -sm) binary_available=1 binary_error="" case "$archi" in - "Darwin x86_64") download fzf-$version-darwin_amd64 ;; -# "Darwin i[36]86") download fzf-$version-darwin_386 ;; - "Linux x86_64") download fzf-$version-linux_amd64 ;; -# "Linux i[36]86") download fzf-$version-linux_386 ;; - *) binary_available=0 ;; + Darwin\ x86_64) download fzf-$version-darwin_amd64 ;; + Darwin\ i*86) download fzf-$version-darwin_386 ;; + Linux\ x86_64) download fzf-$version-linux_amd64 ;; + Linux\ i*86) download fzf-$version-linux_386 ;; + *) binary_available=0 ;; esac cd "$fzf_base" diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch index 9fa4ea3..8f942db 100644 --- a/src/Dockerfile.arch +++ b/src/Dockerfile.arch @@ -13,6 +13,12 @@ ENV GOPATH /go ENV GOROOT /go1.4 ENV PATH /go1.4/bin:$PATH +# For i386 build +RUN echo '[multilib]' >> /etc/pacman.conf && \ + echo 'Include = /etc/pacman.d/mirrorlist' >> /etc/pacman.conf && \ + pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \ + cd $GOROOT/src && GOARCH=386 ./make.bash + # Symlink fzf directory RUN mkdir -p /go/src/github.com/junegunn && \ ln -s /fzf /go/src/github.com/junegunn/fzf diff --git a/src/Makefile b/src/Makefile index 3e57a17..12767d3 100644 --- a/src/Makefile +++ b/src/Makefile @@ -45,7 +45,7 @@ clean: cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz # Linux distribution to build fzf on -DISTRO := ubuntu +DISTRO := arch docker: docker build -t junegunn/$(DISTRO)-sandbox - < Dockerfile.$(DISTRO) diff --git a/src/README.md b/src/README.md index 70b2c1d..06b915a 100644 --- a/src/README.md +++ b/src/README.md @@ -29,10 +29,9 @@ make linux System requirements ------------------- -Currently prebuilt binaries are provided only for 64 bit OS X and Linux. -The install script will fall back to the legacy Ruby version on the other -systems, but if you have Go installed, you can try building it yourself. -(`make install`) +Currently prebuilt binaries are provided only for OS X and Linux. The install +script will fall back to the legacy Ruby version on the other systems, but if +you have Go installed, you can try building it yourself. (`make install`) However, as pointed out in [golang.org/doc/install][req], the Go version will not run on CentOS/RHEL 5.x and thus the install script will choose the Ruby diff --git a/src/curses/curses.go b/src/curses/curses.go index 945a3ce..e4a6575 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -1,8 +1,15 @@ package curses -// #include -// #include -// #cgo LDFLAGS: -lncurses +/* +#include +#include +#cgo LDFLAGS: -lncurses +void swapOutput() { + FILE* temp = stdout; + stdout = stderr; + stderr = temp; +} +*/ import "C" import ( @@ -162,7 +169,7 @@ func Init(color bool, color256 bool, black bool, mouse bool) { // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) } - swapOutput() + C.swapOutput() C.setlocale(C.LC_ALL, C.CString("")) C.initscr() @@ -218,13 +225,7 @@ func Init(color bool, color256 bool, black bool, mouse bool) { func Close() { C.endwin() - swapOutput() -} - -func swapOutput() { - syscall.Dup2(2, 3) - syscall.Dup2(1, 2) - syscall.Dup2(3, 1) + C.swapOutput() } func GetBytes() []byte { From 8a0ab20a70954a5957a648a77a64e05013fbdf9a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 7 Jan 2015 01:14:35 +0900 Subject: [PATCH 023/408] Update vim plugin to use Go binary --- plugin/fzf.vim | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index db3c649..22fb4cc 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -1,4 +1,4 @@ -" Copyright (c) 2014 Junegunn Choi +" Copyright (c) 2015 Junegunn Choi " " MIT License " @@ -25,6 +25,7 @@ let s:min_tmux_width = 10 let s:min_tmux_height = 3 let s:default_tmux_height = '40%' let s:launcher = 'xterm -e bash -ic %s' +let s:fzf_go = expand(':h:h').'/bin/fzf' let s:fzf_rb = expand(':h:h').'/fzf' let s:cpo_save = &cpo @@ -34,7 +35,8 @@ function! s:fzf_exec() if !exists('s:exec') call system('type fzf') if v:shell_error - let s:exec = executable(s:fzf_rb) ? s:fzf_rb : '' + let s:exec = executable(s:fzf_go) ? + \ s:fzf_go : (executable(s:fzf_rb) ? s:fzf_rb : '') else let s:exec = 'fzf' endif From 3e129ac68c08d63d304b9d4bf7229ef1152c4163 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 7 Jan 2015 02:24:13 +0900 Subject: [PATCH 024/408] Remove extraneous quote-escape --- src/reader.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/reader.go b/src/reader.go index 0e1f0a9..39fa70c 100644 --- a/src/reader.go +++ b/src/reader.go @@ -5,13 +5,12 @@ import "C" import ( "bufio" - "fmt" "io" "os" "os/exec" ) -const DEFAULT_COMMAND = "find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null" +const DEFAULT_COMMAND = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` type Reader struct { pusher func(string) @@ -45,8 +44,7 @@ func (r *Reader) readFromStdin() { } func (r *Reader) readFromCommand(cmd string) { - arg := fmt.Sprintf("%q", cmd) - listCommand := exec.Command("sh", "-c", arg[1:len(arg)-1]) + listCommand := exec.Command("sh", "-c", cmd) out, err := listCommand.StdoutPipe() if err != nil { return From f99f66570bdbc296f021ec102b41ab944c7b74a3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 7 Jan 2015 12:46:45 +0900 Subject: [PATCH 025/408] Add small initial delay to screen update To avoid flickering when the input is small --- src/terminal.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index a935bd3..32c8458 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -31,6 +31,7 @@ type Terminal struct { eventBox *EventBox mutex sync.Mutex initFunc func() + suppress bool } var _spinner []string = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} @@ -39,11 +40,17 @@ const ( REQ_PROMPT EventType = iota REQ_INFO REQ_LIST + REQ_REFRESH REQ_REDRAW REQ_CLOSE REQ_QUIT ) +const ( + INITIAL_DELAY = 100 * time.Millisecond + SPINNER_DURATION = 200 * time.Millisecond +) + func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { input := []rune(opts.Query) return &Terminal{ @@ -62,6 +69,7 @@ func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { reqBox: NewEventBox(), eventBox: eventBox, mutex: sync.Mutex{}, + suppress: true, initFunc: func() { C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse) }} @@ -79,6 +87,9 @@ func (t *Terminal) UpdateCount(cnt int, final bool) { t.reading = !final t.mutex.Unlock() t.reqBox.Set(REQ_INFO, nil) + if final { + t.reqBox.Set(REQ_REFRESH, nil) + } } func (t *Terminal) UpdateProgress(progress float32) { @@ -158,7 +169,7 @@ func (t *Terminal) printPrompt() { func (t *Terminal) printInfo() { t.move(1, 0, true) if t.reading { - duration := int64(200) * int64(time.Millisecond) + duration := int64(SPINNER_DURATION) idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration C.CPrint(C.COL_SPINNER, true, _spinner[idx]) } @@ -297,7 +308,9 @@ func (t *Terminal) printAll() { } func (t *Terminal) refresh() { - C.Refresh() + if !t.suppress { + C.Refresh() + } } func (t *Terminal) delChar() bool { @@ -350,11 +363,16 @@ func (t *Terminal) Loop() { { // Late initialization t.mutex.Lock() t.initFunc() - t.printInfo() t.printPrompt() t.placeCursor() - t.refresh() + C.Refresh() + t.printInfo() t.mutex.Unlock() + go func() { + timer := time.NewTimer(INITIAL_DELAY) + <-timer.C + t.reqBox.Set(REQ_REFRESH, nil) + }() } go func() { @@ -370,6 +388,8 @@ func (t *Terminal) Loop() { t.printInfo() case REQ_LIST: t.printList() + case REQ_REFRESH: + t.suppress = false case REQ_REDRAW: C.Clear() t.printAll() From 23f27f3ce539e324b5c2c4919a059e0845edcf12 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 7 Jan 2015 20:08:05 +0900 Subject: [PATCH 026/408] Improve install script --- install | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/install b/install index ce5ec79..f8307cf 100755 --- a/install +++ b/install @@ -12,21 +12,25 @@ ask() { } check_binary() { - echo "- Checking fzf executable" - echo -n " - " + echo -n " - Checking fzf executable ... " if ! "$fzf_base"/bin/fzf --version; then - rm -v "$fzf_base"/bin/fzf + rm -f "$fzf_base"/bin/fzf binary_error="Error occurred" fi } +symlink() { + echo " - Creating symlink: bin/$1 -> bin/fzf" + rm -f "$fzf_base"/bin/fzf + ln -sf "$fzf_base"/bin/$1 "$fzf_base"/bin/fzf +} + download() { - echo "Downloading fzf executable ($1) ..." - if [ -x "$fzf_base"/bin/fzf ]; then - if ! ask "- fzf already exists. Download it again?"; then - check_binary - return - fi + echo "Downloading bin/$1 ..." + if [ -x "$fzf_base"/bin/$1 ]; then + echo " - Already exists" + symlink $1 + check_binary && return fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin if [ $? -ne 0 ]; then @@ -49,7 +53,7 @@ download() { return fi - mv $1 fzf && chmod +x fzf && check_binary + chmod +x $1 && symlink $1 && check_binary } # Try to download binary executable @@ -69,7 +73,7 @@ if [ -n "$binary_error" ]; then if [ $binary_available -eq 0 ]; then echo "No prebuilt binary for $archi ... " else - echo " - $binary_error ... " + echo " - $binary_error !!!" fi echo "Installing legacy Ruby version ..." @@ -77,7 +81,7 @@ if [ -n "$binary_error" ]; then echo -n "Checking Ruby executable ... " ruby=`which ruby` if [ $? -ne 0 ]; then - echo "ruby executable not found!" + echo "ruby executable not found !!!" exit 1 fi From 3ed86445e1906eb47679bd2c8a5ee1a01632c883 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 8 Jan 2015 11:04:25 +0900 Subject: [PATCH 027/408] Remove call to ncurses set_tabsize() Not available on old verions of ncurses --- src/curses/curses.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index e4a6575..736ccf6 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -178,8 +178,7 @@ func Init(color bool, color256 bool, black bool, mouse bool) { } C.cbreak() C.noecho() - C.raw() // stty dsusp undef - C.set_tabsize(4) // FIXME + C.raw() // stty dsusp undef intChan := make(chan os.Signal, 1) signal.Notify(intChan, os.Interrupt, os.Kill) From efec9acd6f655c7e63d6cda61486c961fdaed443 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 8 Jan 2015 22:04:12 +0900 Subject: [PATCH 028/408] Fix missing mutex unlock --- src/terminal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/terminal.go b/src/terminal.go index 32c8458..a442d34 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -434,6 +434,7 @@ func (t *Terminal) Loop() { } switch event.Type { case C.INVALID: + t.mutex.Unlock() continue case C.CTRL_A: t.cx = 0 From f401c42f9c22de9df7a40ee31727ff0eab5dd30e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 8 Jan 2015 22:07:04 +0900 Subject: [PATCH 029/408] Adjust initial coordinator delay --- src/core.go | 10 +++++++--- src/util.go | 13 +++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/core.go b/src/core.go index b6f0857..5a81efa 100644 --- a/src/core.go +++ b/src/core.go @@ -7,7 +7,8 @@ import ( "time" ) -const COORDINATOR_DELAY time.Duration = 100 * time.Millisecond +const COORDINATOR_DELAY_MAX time.Duration = 100 * time.Millisecond +const COORDINATOR_DELAY_STEP time.Duration = 10 * time.Millisecond func initProcs() { runtime.GOMAXPROCS(runtime.NumCPU()) @@ -151,8 +152,11 @@ func Run(options *Options) { } } }) - if ticks > 3 && delay && reading { - time.Sleep(COORDINATOR_DELAY) + if delay && reading { + dur := DurWithin( + time.Duration(ticks)*COORDINATOR_DELAY_STEP, + 0, COORDINATOR_DELAY_MAX) + time.Sleep(dur) } } } diff --git a/src/util.go b/src/util.go index 2144e54..cc8d4f5 100644 --- a/src/util.go +++ b/src/util.go @@ -1,5 +1,7 @@ package fzf +import "time" + func Max(first int, items ...int) int { max := first for _, item := range items { @@ -19,3 +21,14 @@ func Min(first int, items ...int) int { } return min } + +func DurWithin( + val time.Duration, min time.Duration, max time.Duration) time.Duration { + if val < min { + return min + } + if val > max { + return max + } + return val +} From d303c5b3ebc6d56af6d3a03c6b4cdb361a2b022c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 9 Jan 2015 02:35:20 +0900 Subject: [PATCH 030/408] Minor refactoring --- src/algo.go | 2 +- src/tokenizer.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/algo.go b/src/algo.go index 16790ba..e0c173f 100644 --- a/src/algo.go +++ b/src/algo.go @@ -90,7 +90,7 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in runes := []rune(*input) numRunes := len(runes) plen := len(pattern) - if len(runes) < plen { + if numRunes < plen { return -1, -1 } diff --git a/src/tokenizer.go b/src/tokenizer.go index bc1ca3a..d62f395 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -118,14 +118,13 @@ func awkTokenizer(input *string) ([]string, int) { } func Tokenize(str *string, delimiter *regexp.Regexp) []Token { - prefixLength := 0 if delimiter == nil { // AWK-style (\S+\s*) tokens, prefixLength := awkTokenizer(str) return withPrefixLengths(tokens, prefixLength) } else { tokens := delimiter.FindAllString(*str, -1) - return withPrefixLengths(tokens, prefixLength) + return withPrefixLengths(tokens, 0) } } From aa05bf5206768965e575b6032543745c830e6eea Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 9 Jan 2015 02:37:08 +0900 Subject: [PATCH 031/408] Reduce memory footprint --- src/chunklist_test.go | 6 +++--- src/core.go | 11 ++++++----- src/item.go | 41 +++++++++++++++++++++++++---------------- src/item_test.go | 17 ++++++++--------- src/pattern.go | 10 ++++------ src/terminal.go | 18 +++++++++--------- src/util.go | 7 +++++++ 7 files changed, 62 insertions(+), 48 deletions(-) diff --git a/src/chunklist_test.go b/src/chunklist_test.go index b244ece..09e4aad 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -7,7 +7,7 @@ import ( func TestChunkList(t *testing.T) { cl := NewChunkList(func(s *string, i int) *Item { - return &Item{text: s, index: i * 2} + return &Item{text: s, rank: Rank{0, 0, uint32(i * 2)}} }) // Snapshot @@ -36,8 +36,8 @@ func TestChunkList(t *testing.T) { if len(*chunk1) != 2 { t.Error("Snapshot should contain only two items") } - if *(*chunk1)[0].text != "hello" || (*chunk1)[0].index != 0 || - *(*chunk1)[1].text != "world" || (*chunk1)[1].index != 2 { + if *(*chunk1)[0].text != "hello" || (*chunk1)[0].rank.index != 0 || + *(*chunk1)[1].text != "world" || (*chunk1)[1].rank.index != 2 { t.Error("Invalid data") } if chunk1.IsFull() { diff --git a/src/core.go b/src/core.go index 5a81efa..e5bdb12 100644 --- a/src/core.go +++ b/src/core.go @@ -39,14 +39,15 @@ func Run(options *Options) { var chunkList *ChunkList if len(opts.WithNth) == 0 { chunkList = NewChunkList(func(data *string, index int) *Item { - return &Item{text: data, index: index} + return &Item{text: data, rank: Rank{0, 0, uint32(index)}} }) } else { chunkList = NewChunkList(func(data *string, index int) *Item { - item := Item{text: data, index: index} - tokens := Tokenize(item.text, opts.Delimiter) - item.origText = item.text - item.text = Transform(tokens, opts.WithNth).whole + tokens := Tokenize(data, opts.Delimiter) + item := Item{ + text: Transform(tokens, opts.WithNth).whole, + origText: data, + rank: Rank{0, 0, uint32(index)}} return &item }) } diff --git a/src/item.go b/src/item.go index b70da93..4c8f13d 100644 --- a/src/item.go +++ b/src/item.go @@ -5,31 +5,32 @@ import ( "sort" ) -type Offset [2]int +type Offset [2]int32 type Item struct { text *string origText *string offsets []Offset - index int rank Rank transformed *Transformed } -type Rank [3]int - -var NilRank = Rank{-1, 0, 0} +type Rank struct { + matchlen uint16 + strlen uint16 + index uint32 +} func (i *Item) Rank() Rank { - if i.rank[0] > 0 { + if i.rank.matchlen > 0 || i.rank.strlen > 0 { return i.rank } sort.Sort(ByOrder(i.offsets)) matchlen := 0 prevEnd := 0 for _, offset := range i.offsets { - begin := offset[0] - end := offset[1] + begin := int(offset[0]) + end := int(offset[1]) if prevEnd > begin { begin = prevEnd } @@ -40,7 +41,7 @@ func (i *Item) Rank() Rank { matchlen += end - begin } } - i.rank = Rank{matchlen, len(*i.text), i.index} + i.rank = Rank{uint16(matchlen), uint16(len(*i.text)), i.rank.index} return i.rank } @@ -86,14 +87,22 @@ func (a ByRelevance) Less(i, j int) bool { } func compareRanks(irank Rank, jrank Rank) bool { - for idx := range irank { - if irank[idx] < jrank[idx] { - return true - } else if irank[idx] > jrank[idx] { - return false - } + if irank.matchlen < jrank.matchlen { + return true + } else if irank.matchlen > jrank.matchlen { + return false } - return true + + if irank.strlen < jrank.strlen { + return true + } else if irank.strlen > jrank.strlen { + return false + } + + if irank.index <= jrank.index { + return true + } + return false } func SortMerge(partialResults [][]*Item) []*Item { diff --git a/src/item_test.go b/src/item_test.go index 1e31629..23b8718 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -23,8 +23,7 @@ func TestRankComparison(t *testing.T) { if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}) || !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}) || !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}) || - !compareRanks(NilRank, Rank{0, 0, 0}) || - compareRanks(Rank{0, 0, 0}, NilRank) { + !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}) { t.Error("Invalid order") } } @@ -32,13 +31,13 @@ func TestRankComparison(t *testing.T) { // Match length, string length, index func TestItemRank(t *testing.T) { strs := []string{"foo", "foobar", "bar", "baz"} - item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}} + item1 := Item{text: &strs[0], rank: Rank{0, 0, 1}, offsets: []Offset{}} rank1 := item1.Rank() - if rank1[0] != 0 || rank1[1] != 3 || rank1[2] != 1 { + if rank1.matchlen != 0 || rank1.strlen != 3 || rank1.index != 1 { t.Error(item1.Rank()) } // Only differ in index - item2 := Item{text: &strs[0], index: 0, offsets: []Offset{}} + item2 := Item{text: &strs[0], rank: Rank{0, 0, 0}, offsets: []Offset{}} items := []*Item{&item1, &item2} sort.Sort(ByRelevance(items)) @@ -54,10 +53,10 @@ func TestItemRank(t *testing.T) { } // Sort by relevance - item3 := Item{text: &strs[1], index: 2, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item4 := Item{text: &strs[1], index: 2, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} - item5 := Item{text: &strs[2], index: 2, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item6 := Item{text: &strs[2], index: 2, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item3 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item4 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item5 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item6 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} sort.Sort(ByRelevance(items)) if items[0] != &item2 || items[1] != &item1 || diff --git a/src/pattern.go b/src/pattern.go index 7c27f52..7b29425 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -236,9 +236,8 @@ func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { if sidx, eidx := p.iter(FuzzyMatch, input, p.text); sidx >= 0 { matches = append(matches, &Item{ text: item.text, - index: item.index, - offsets: []Offset{Offset{sidx, eidx}}, - rank: NilRank}) + offsets: []Offset{Offset{int32(sidx), int32(eidx)}}, + rank: Rank{0, 0, item.rank.index}}) } } return matches @@ -256,7 +255,7 @@ func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { if term.inv { break Loop } - offsets = append(offsets, Offset{sidx, eidx}) + offsets = append(offsets, Offset{int32(sidx), int32(eidx)}) } else if term.inv { offsets = append(offsets, Offset{0, 0}) } @@ -264,9 +263,8 @@ func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { if len(offsets) == len(p.terms) { matches = append(matches, &Item{ text: item.text, - index: item.index, offsets: offsets, - rank: NilRank}) + rank: Rank{0, 0, item.rank.index}}) } } return matches diff --git a/src/terminal.go b/src/terminal.go index a442d34..77a70f7 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -232,9 +232,9 @@ func trimRight(runes []rune, width int) ([]rune, int) { return runes, trimmed } -func trimLeft(runes []rune, width int) ([]rune, int) { +func trimLeft(runes []rune, width int) ([]rune, int32) { currentWidth := displayWidth(runes) - trimmed := 0 + var trimmed int32 = 0 for currentWidth > width && len(runes) > 0 { currentWidth -= runewidth.RuneWidth(runes[0]) @@ -245,7 +245,7 @@ func trimLeft(runes []rune, width int) ([]rune, int) { } func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { - maxe := 0 + var maxe int32 = 0 for _, offset := range item.offsets { if offset[1] > maxe { maxe = offset[1] @@ -269,7 +269,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { text = append(text[:maxe], []rune("..")...) } // ..ri.. - var diff int + var diff int32 text, diff = trimLeft(text, maxWidth-2) // Transform offsets @@ -278,7 +278,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { b, e := offset[0], offset[1] b += 2 - diff e += 2 - diff - b = Max(b, 2) + b = Max32(b, 2) if b < e { offsets[idx] = Offset{b, e} } @@ -288,15 +288,15 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { } sort.Sort(ByOrder(offsets)) - index := 0 + var index int32 = 0 for _, offset := range offsets { - b := Max(index, offset[0]) - e := Max(index, offset[1]) + b := Max32(index, offset[0]) + e := Max32(index, offset[1]) C.CPrint(col1, bold, string(text[index:b])) C.CPrint(col2, bold, string(text[b:e])) index = e } - if index < len(text) { + if index < int32(len(text)) { C.CPrint(col1, bold, string(text[index:])) } } diff --git a/src/util.go b/src/util.go index cc8d4f5..de6f365 100644 --- a/src/util.go +++ b/src/util.go @@ -12,6 +12,13 @@ func Max(first int, items ...int) int { return max } +func Max32(first int32, second int32) int32 { + if first > second { + return first + } + return second +} + func Min(first int, items ...int) int { min := first for _, item := range items { From b7bb1008107fb079e68f9ebeeca699c65cc966c9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 01:06:08 +0900 Subject: [PATCH 032/408] Improve response time by only looking at top-N items --- src/core.go | 11 ++++--- src/item.go | 38 ---------------------- src/item_test.go | 10 ------ src/matcher.go | 39 ++++++++-------------- src/merger.go | 79 +++++++++++++++++++++++++++++++++++++++++++++ src/merger_test.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++ src/terminal.go | 28 ++++++++-------- 7 files changed, 193 insertions(+), 92 deletions(-) create mode 100644 src/merger.go create mode 100644 src/merger_test.go diff --git a/src/core.go b/src/core.go index e5bdb12..98973f8 100644 --- a/src/core.go +++ b/src/core.go @@ -93,17 +93,18 @@ func Run(options *Options) { } snapshot, _ := chunkList.Snapshot() - matches, cancelled := matcher.scan(MatchRequest{ + merger, cancelled := matcher.scan(MatchRequest{ chunks: snapshot, pattern: pattern}, limit) if !cancelled && (filtering || - opts.Exit0 && len(matches) == 0 || opts.Select1 && len(matches) == 1) { + opts.Exit0 && merger.Length() == 0 || + opts.Select1 && merger.Length() == 1) { if opts.PrintQuery { fmt.Println(patternString) } - for _, item := range matches { - item.Print() + for i := 0; i < merger.Length(); i++ { + merger.Get(i).Print() } os.Exit(0) } @@ -147,7 +148,7 @@ func Run(options *Options) { case EVT_SEARCH_FIN: switch val := value.(type) { - case []*Item: + case *Merger: terminal.UpdateList(val) } } diff --git a/src/item.go b/src/item.go index 4c8f13d..60355b4 100644 --- a/src/item.go +++ b/src/item.go @@ -104,41 +104,3 @@ func compareRanks(irank Rank, jrank Rank) bool { } return false } - -func SortMerge(partialResults [][]*Item) []*Item { - if len(partialResults) == 1 { - return partialResults[0] - } - - merged := []*Item{} - - for len(partialResults) > 0 { - minRank := Rank{0, 0, 0} - minIdx := -1 - - for idx, partialResult := range partialResults { - if len(partialResult) > 0 { - rank := partialResult[0].Rank() - if minIdx < 0 || compareRanks(rank, minRank) { - minRank = rank - minIdx = idx - } - } - } - - if minIdx >= 0 { - merged = append(merged, partialResults[minIdx][0]) - partialResults[minIdx] = partialResults[minIdx][1:] - } - - nonEmptyPartialResults := make([][]*Item, 0, len(partialResults)) - for _, partialResult := range partialResults { - if len(partialResult) > 0 { - nonEmptyPartialResults = append(nonEmptyPartialResults, partialResult) - } - } - partialResults = nonEmptyPartialResults - } - - return merged -} diff --git a/src/item_test.go b/src/item_test.go index 23b8718..87d8be4 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -64,14 +64,4 @@ func TestItemRank(t *testing.T) { items[4] != &item5 || items[5] != &item3 { t.Error(items) } - - // Sort merged lists - lists := [][]*Item{ - []*Item{&item2, &item4, &item5}, []*Item{&item1, &item6}, []*Item{&item3}} - items = SortMerge(lists) - if items[0] != &item2 || items[1] != &item1 || - items[2] != &item6 || items[3] != &item4 || - items[4] != &item5 || items[5] != &item3 { - t.Error(items) - } } diff --git a/src/matcher.go b/src/matcher.go index ad782bd..234e703 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -18,7 +18,7 @@ type Matcher struct { eventBox *EventBox reqBox *EventBox partitions int - queryCache QueryCache + mergerCache map[string]*Merger } const ( @@ -44,7 +44,7 @@ func NewMatcher(patternBuilder func([]rune) *Pattern, eventBox: eventBox, reqBox: NewEventBox(), partitions: runtime.NumCPU(), - queryCache: make(QueryCache)} + mergerCache: make(map[string]*Merger)} } func (m *Matcher) Loop() { @@ -67,30 +67,30 @@ func (m *Matcher) Loop() { // Restart search patternString := request.pattern.AsString() - allMatches := []*Item{} + var merger *Merger cancelled := false count := CountItems(request.chunks) foundCache := false if count == prevCount { - // Look up queryCache - if cached, found := m.queryCache[patternString]; found { + // Look up mergerCache + if cached, found := m.mergerCache[patternString]; found { foundCache = true - allMatches = cached + merger = cached } } else { - // Invalidate queryCache + // Invalidate mergerCache prevCount = count - m.queryCache = make(QueryCache) + m.mergerCache = make(map[string]*Merger) } if !foundCache { - allMatches, cancelled = m.scan(request, 0) + merger, cancelled = m.scan(request, 0) } if !cancelled { - m.queryCache[patternString] = allMatches - m.eventBox.Set(EVT_SEARCH_FIN, allMatches) + m.mergerCache[patternString] = merger + m.eventBox.Set(EVT_SEARCH_FIN, merger) } } } @@ -120,12 +120,12 @@ type partialResult struct { matches []*Item } -func (m *Matcher) scan(request MatchRequest, limit int) ([]*Item, bool) { +func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { startedAt := time.Now() numChunks := len(request.chunks) if numChunks == 0 { - return []*Item{}, false + return EmptyMerger, false } pattern := request.pattern empty := pattern.IsEmpty() @@ -188,18 +188,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) ([]*Item, bool) { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches } - - var allMatches []*Item - if empty || !m.sort { - allMatches = []*Item{} - for _, matches := range partialResults { - allMatches = append(allMatches, matches...) - } - } else { - allMatches = SortMerge(partialResults) - } - - return allMatches, false + return NewMerger(partialResults, !empty && m.sort), false } func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { diff --git a/src/merger.go b/src/merger.go new file mode 100644 index 0000000..7ff3243 --- /dev/null +++ b/src/merger.go @@ -0,0 +1,79 @@ +package fzf + +var EmptyMerger *Merger = NewMerger([][]*Item{}, false) + +type Merger struct { + lists [][]*Item + merged []*Item + cursors []int + done bool +} + +func NewMerger(lists [][]*Item, sorted bool) *Merger { + mg := Merger{ + lists: lists, + merged: []*Item{}, + cursors: make([]int, len(lists)), + done: false} + if !sorted { + for _, list := range lists { + mg.merged = append(mg.merged, list...) + } + mg.done = true + } + return &mg +} + +func (mg *Merger) Length() int { + cnt := 0 + for _, list := range mg.lists { + cnt += len(list) + } + return cnt +} + +func (mg *Merger) Get(idx int) *Item { + if mg.done { + return mg.merged[idx] + } else if len(mg.lists) == 1 { + return mg.lists[0][idx] + } + mg.buildUpto(idx) + return mg.merged[idx] +} + +func (mg *Merger) buildUpto(upto int) { + numBuilt := len(mg.merged) + if numBuilt > upto { + return + } + + for i := numBuilt; i <= upto; i++ { + minRank := Rank{0, 0, 0} + minIdx := -1 + for listIdx, list := range mg.lists { + cursor := mg.cursors[listIdx] + if cursor < 0 || cursor == len(list) { + mg.cursors[listIdx] = -1 + continue + } + if cursor >= 0 { + rank := list[cursor].Rank() + if minIdx < 0 || compareRanks(rank, minRank) { + minRank = rank + minIdx = listIdx + } + } + mg.cursors[listIdx] = cursor + } + + if minIdx >= 0 { + chosen := mg.lists[minIdx] + mg.merged = append(mg.merged, chosen[mg.cursors[minIdx]]) + mg.cursors[minIdx] += 1 + } else { + mg.done = true + return + } + } +} diff --git a/src/merger_test.go b/src/merger_test.go new file mode 100644 index 0000000..19941b1 --- /dev/null +++ b/src/merger_test.go @@ -0,0 +1,80 @@ +package fzf + +import ( + "math/rand" + "sort" + "testing" +) + +func assert(t *testing.T, cond bool, msg ...string) { + if !cond { + t.Error(msg) + } +} + +func randItem() *Item { + return &Item{ + rank: Rank{uint16(rand.Uint32()), uint16(rand.Uint32()), rand.Uint32()}} +} + +func TestEmptyMerger(t *testing.T) { + assert(t, EmptyMerger.Length() == 0, "Not empty") +} + +func buildLists(partiallySorted bool) ([][]*Item, []*Item) { + numLists := 4 + lists := make([][]*Item, numLists) + cnt := 0 + for i := 0; i < numLists; i++ { + numItems := rand.Int() % 20 + cnt += numItems + lists[i] = make([]*Item, numItems) + for j := 0; j < numItems; j++ { + item := randItem() + lists[i][j] = item + } + if partiallySorted { + sort.Sort(ByRelevance(lists[i])) + } + } + items := []*Item{} + for _, list := range lists { + items = append(items, list...) + } + return lists, items +} + +func TestMergerUnsorted(t *testing.T) { + lists, items := buildLists(false) + cnt := len(items) + + // Not sorted: same order + mg := NewMerger(lists, false) + assert(t, cnt == mg.Length(), "Invalid Length") + for i := 0; i < cnt; i++ { + assert(t, items[i] == mg.Get(i), "Invalid Get") + } +} + +func TestMergerSorted(t *testing.T) { + lists, items := buildLists(true) + cnt := len(items) + + // Sorted sorted order + mg := NewMerger(lists, true) + assert(t, cnt == mg.Length(), "Invalid Length") + sort.Sort(ByRelevance(items)) + for i := 0; i < cnt; i++ { + if items[i] != mg.Get(i) { + t.Error("Not sorted", items[i], mg.Get(i)) + } + } + + // Inverse order + mg2 := NewMerger(lists, true) + for i := cnt - 1; i >= cnt; i-- { + if items[i] != mg2.Get(i) { + t.Error("Not sorted", items[i], mg2.Get(i)) + } + } +} diff --git a/src/terminal.go b/src/terminal.go index 77a70f7..7b83a47 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -25,7 +25,7 @@ type Terminal struct { count int progress int reading bool - list []*Item + merger *Merger selected map[*string]*string reqBox *EventBox eventBox *EventBox @@ -64,7 +64,7 @@ func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { input: input, multi: opts.Multi, printQuery: opts.PrintQuery, - list: []*Item{}, + merger: EmptyMerger, selected: make(map[*string]*string), reqBox: NewEventBox(), eventBox: eventBox, @@ -99,10 +99,10 @@ func (t *Terminal) UpdateProgress(progress float32) { t.reqBox.Set(REQ_INFO, nil) } -func (t *Terminal) UpdateList(list []*Item) { +func (t *Terminal) UpdateList(merger *Merger) { t.mutex.Lock() t.progress = 100 - t.list = list + t.merger = merger t.mutex.Unlock() t.reqBox.Set(REQ_INFO, nil) t.reqBox.Set(REQ_LIST, nil) @@ -110,7 +110,7 @@ func (t *Terminal) UpdateList(list []*Item) { func (t *Terminal) listIndex(y int) int { if t.tac { - return len(t.list) - y - 1 + return t.merger.Length() - y - 1 } else { return y } @@ -121,8 +121,8 @@ func (t *Terminal) output() { fmt.Println(string(t.input)) } if len(t.selected) == 0 { - if len(t.list) > t.cy { - t.list[t.listIndex(t.cy)].Print() + if t.merger.Length() > t.cy { + t.merger.Get(t.listIndex(t.cy)).Print() } } else { for ptr, orig := range t.selected { @@ -175,7 +175,7 @@ func (t *Terminal) printInfo() { } t.move(1, 2, false) - output := fmt.Sprintf("%d/%d", len(t.list), t.count) + output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) if t.multi && len(t.selected) > 0 { output += fmt.Sprintf(" (%d)", len(t.selected)) } @@ -189,11 +189,11 @@ func (t *Terminal) printList() { t.constrain() maxy := maxItems() - count := len(t.list) - t.offset + count := t.merger.Length() - t.offset for i := 0; i < maxy; i++ { t.move(i+2, 0, true) if i < count { - t.printItem(t.list[t.listIndex(i+t.offset)], i == t.cy-t.offset) + t.printItem(t.merger.Get(t.listIndex(i+t.offset)), i == t.cy-t.offset) } } } @@ -417,7 +417,7 @@ func (t *Terminal) Loop() { previousInput := t.input events := []EventType{REQ_PROMPT} toggle := func() { - item := t.list[t.listIndex(t.cy)] + item := t.merger.Get(t.listIndex(t.cy)) if _, found := t.selected[item.text]; !found { t.selected[item.text] = item.origText } else { @@ -460,13 +460,13 @@ func (t *Terminal) Loop() { t.cx -= 1 } case C.TAB: - if t.multi && len(t.list) > 0 { + if t.multi && t.merger.Length() > 0 { toggle() t.vmove(-1) req(REQ_LIST, REQ_INFO) } case C.BTAB: - if t.multi && len(t.list) > 0 { + if t.multi && t.merger.Length() > 0 { toggle() t.vmove(1) req(REQ_LIST, REQ_INFO) @@ -567,7 +567,7 @@ func (t *Terminal) Loop() { } func (t *Terminal) constrain() { - count := len(t.list) + count := t.merger.Length() height := C.MaxY() - 2 diffpos := t.cy - t.offset From 8b02ae650c99b3fffe9c572e96ada007cc1ccad5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 01:16:13 +0900 Subject: [PATCH 033/408] Update src/README.md --- src/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/README.md b/src/README.md index 06b915a..f179ae7 100644 --- a/src/README.md +++ b/src/README.md @@ -2,12 +2,13 @@ fzf in Go ========= This directory contains the source code for the new fzf implementation in -[Go][go]. This new version has the following benefits over the previous Ruby +[Go][go]. The new version has the following benefits over the previous Ruby version. - Immensely faster - No GIL. Performance is linearly proportional to the number of cores. - - It's so fast that I even decided to remove the sort limit (`--sort=N`) + - It's so fast that I even decided to remove the sort limit. `--sort=N` is + no longer required. - Does not require Ruby and distributed as an executable binary - Ruby dependency is especially painful on Ruby 2.1 or above which ships without curses gem @@ -16,7 +17,7 @@ Build ----- ```sh -# Build fzf executable +# Build fzf executables make # Install the executable to ../bin directory @@ -31,9 +32,9 @@ System requirements Currently prebuilt binaries are provided only for OS X and Linux. The install script will fall back to the legacy Ruby version on the other systems, but if -you have Go installed, you can try building it yourself. (`make install`) +you have Go 1.4 installed, you can try building it yourself. (`make install`) -However, as pointed out in [golang.org/doc/install][req], the Go version will +However, as pointed out in [golang.org/doc/install][req], the Go version may not run on CentOS/RHEL 5.x and thus the install script will choose the Ruby version instead. From 188c90bf2564adcade3eb283ecf72fa74b7dc6dd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 12:21:17 +0900 Subject: [PATCH 034/408] Fix incorrect behaviors of mouse events when --multi enabled --- src/terminal.go | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 7b83a47..73bf396 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -416,14 +416,6 @@ func (t *Terminal) Loop() { t.mutex.Lock() previousInput := t.input events := []EventType{REQ_PROMPT} - toggle := func() { - item := t.merger.Get(t.listIndex(t.cy)) - if _, found := t.selected[item.text]; !found { - t.selected[item.text] = item.origText - } else { - delete(t.selected, item.text) - } - } req := func(evts ...EventType) { for _, event := range evts { events = append(events, event) @@ -432,6 +424,18 @@ func (t *Terminal) Loop() { } } } + toggle := func() { + idx := t.listIndex(t.cy) + if idx < t.merger.Length() { + item := t.merger.Get(idx) + if _, found := t.selected[item.text]; !found { + t.selected[item.text] = item.origText + } else { + delete(t.selected, item.text) + } + req(REQ_INFO) + } + } switch event.Type { case C.INVALID: t.mutex.Unlock() @@ -463,13 +467,13 @@ func (t *Terminal) Loop() { if t.multi && t.merger.Length() > 0 { toggle() t.vmove(-1) - req(REQ_LIST, REQ_INFO) + req(REQ_LIST) } case C.BTAB: if t.multi && t.merger.Length() > 0 { toggle() t.vmove(1) - req(REQ_LIST, REQ_INFO) + req(REQ_LIST) } case C.CTRL_J, C.CTRL_N: t.vmove(-1) @@ -529,11 +533,13 @@ func (t *Terminal) Loop() { } if me.S != 0 { // Scroll - if me.Mod { - toggle() + if t.merger.Length() > 0 { + if t.multi && me.Mod { + toggle() + } + t.vmove(me.S) + req(REQ_LIST) } - t.vmove(me.S) - req(REQ_LIST) } else if me.Double { // Double-click if my >= 2 { @@ -547,7 +553,7 @@ func (t *Terminal) Loop() { } else if my >= 2 { // List t.cy = t.offset + my - 2 - if me.Mod { + if t.multi && me.Mod { toggle() } req(REQ_LIST) From b8a9861f9530ec63467c062f974e93546240fb53 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 12:26:11 +0900 Subject: [PATCH 035/408] Fix double click on an empty row not to close fzf --- src/terminal.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 73bf396..c9db8ff 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -544,7 +544,9 @@ func (t *Terminal) Loop() { // Double-click if my >= 2 { t.cy = my - 2 - req(REQ_CLOSE) + if t.listIndex(t.cy) < t.merger.Length() { + req(REQ_CLOSE) + } } } else if me.Down { if my == 0 && mx >= 0 { From 2d9b38b93eb16b341e91ec5d8eaaa9898f1d68f6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 14:22:00 +0900 Subject: [PATCH 036/408] Constrain cy in vmove() --- src/terminal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/terminal.go b/src/terminal.go index c9db8ff..fb17ce5 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -602,6 +602,7 @@ func (t *Terminal) vmove(o int) { } else { t.cy += o } + t.cy = Max(0, Min(t.cy, t.merger.Length()-1)) } func maxItems() int { From 6e86fee588bdcd769501ab671fa21a8e8e2de828 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 14:24:12 +0900 Subject: [PATCH 037/408] Change Merger implementation on --no-sort --- src/merger.go | 51 +++++++++++++++++++++++----------------------- src/merger_test.go | 5 ++++- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/merger.go b/src/merger.go index 7ff3243..08a3d15 100644 --- a/src/merger.go +++ b/src/merger.go @@ -1,12 +1,15 @@ package fzf +import "fmt" + var EmptyMerger *Merger = NewMerger([][]*Item{}, false) type Merger struct { lists [][]*Item merged []*Item cursors []int - done bool + sorted bool + count int } func NewMerger(lists [][]*Item, sorted bool) *Merger { @@ -14,41 +17,37 @@ func NewMerger(lists [][]*Item, sorted bool) *Merger { lists: lists, merged: []*Item{}, cursors: make([]int, len(lists)), - done: false} - if !sorted { - for _, list := range lists { - mg.merged = append(mg.merged, list...) - } - mg.done = true + sorted: sorted, + count: 0} + + for _, list := range mg.lists { + mg.count += len(list) } return &mg } func (mg *Merger) Length() int { - cnt := 0 - for _, list := range mg.lists { - cnt += len(list) - } - return cnt + return mg.count } func (mg *Merger) Get(idx int) *Item { - if mg.done { - return mg.merged[idx] - } else if len(mg.lists) == 1 { + if len(mg.lists) == 1 { return mg.lists[0][idx] + } else if !mg.sorted { + for _, list := range mg.lists { + numItems := len(list) + if idx < numItems { + return list[idx] + } + idx -= numItems + } + panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) } - mg.buildUpto(idx) - return mg.merged[idx] + return mg.mergedGet(idx) } -func (mg *Merger) buildUpto(upto int) { - numBuilt := len(mg.merged) - if numBuilt > upto { - return - } - - for i := numBuilt; i <= upto; i++ { +func (mg *Merger) mergedGet(idx int) *Item { + for i := len(mg.merged); i <= idx; i++ { minRank := Rank{0, 0, 0} minIdx := -1 for listIdx, list := range mg.lists { @@ -72,8 +71,8 @@ func (mg *Merger) buildUpto(upto int) { mg.merged = append(mg.merged, chosen[mg.cursors[minIdx]]) mg.cursors[minIdx] += 1 } else { - mg.done = true - return + panic(fmt.Sprintf("Index out of bounds (sorted, %d/%d)", i, mg.count)) } } + return mg.merged[idx] } diff --git a/src/merger_test.go b/src/merger_test.go index 19941b1..32a1228 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -19,6 +19,9 @@ func randItem() *Item { func TestEmptyMerger(t *testing.T) { assert(t, EmptyMerger.Length() == 0, "Not empty") + assert(t, EmptyMerger.count == 0, "Invalid count") + assert(t, len(EmptyMerger.lists) == 0, "Invalid lists") + assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") } func buildLists(partiallySorted bool) ([][]*Item, []*Item) { @@ -72,7 +75,7 @@ func TestMergerSorted(t *testing.T) { // Inverse order mg2 := NewMerger(lists, true) - for i := cnt - 1; i >= cnt; i-- { + for i := cnt - 1; i >= 0; i-- { if items[i] != mg2.Get(i) { t.Error("Not sorted", items[i], mg2.Get(i)) } From f670f4f076867d6876bcbc832a9b464bbe4f8f68 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jan 2015 14:50:24 +0900 Subject: [PATCH 038/408] Make sure that cy is properly limited --- src/terminal.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index fb17ce5..7039e57 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -543,8 +543,7 @@ func (t *Terminal) Loop() { } else if me.Double { // Double-click if my >= 2 { - t.cy = my - 2 - if t.listIndex(t.cy) < t.merger.Length() { + if t.vset(my-2) && t.listIndex(t.cy) < t.merger.Length() { req(REQ_CLOSE) } } @@ -554,8 +553,7 @@ func (t *Terminal) Loop() { t.cx = mx } else if my >= 2 { // List - t.cy = t.offset + my - 2 - if t.multi && me.Mod { + if t.vset(t.offset+my-2) && t.multi && me.Mod { toggle() } req(REQ_LIST) @@ -598,11 +596,15 @@ func (t *Terminal) constrain() { func (t *Terminal) vmove(o int) { if t.reverse { - t.cy -= o + t.vset(t.cy - o) } else { - t.cy += o + t.vset(t.cy + o) } - t.cy = Max(0, Min(t.cy, t.merger.Length()-1)) +} + +func (t *Terminal) vset(o int) bool { + t.cy = Max(0, Min(o, t.merger.Length()-1)) + return t.cy == o } func maxItems() int { From 4f4031443365659de357ad4da15af8b5e3245137 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 01:15:44 +0900 Subject: [PATCH 039/408] Fix --with-nth option when query is non-empty --- src/pattern.go | 14 ++++++++------ src/pattern_test.go | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 7b29425..93dbaf9 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -235,9 +235,10 @@ func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { input := p.prepareInput(item) if sidx, eidx := p.iter(FuzzyMatch, input, p.text); sidx >= 0 { matches = append(matches, &Item{ - text: item.text, - offsets: []Offset{Offset{int32(sidx), int32(eidx)}}, - rank: Rank{0, 0, item.rank.index}}) + text: item.text, + origText: item.origText, + offsets: []Offset{Offset{int32(sidx), int32(eidx)}}, + rank: Rank{0, 0, item.rank.index}}) } } return matches @@ -262,9 +263,10 @@ func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { } if len(offsets) == len(p.terms) { matches = append(matches, &Item{ - text: item.text, - offsets: offsets, - rank: Rank{0, 0, item.rank.index}}) + text: item.text, + origText: item.origText, + offsets: offsets, + rank: Rank{0, 0, item.rank.index}}) } } return matches diff --git a/src/pattern_test.go b/src/pattern_test.go index a1ce626..a776e30 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -85,3 +85,21 @@ func TestCaseSensitivity(t *testing.T) { t.Error("Invalid case conversion") } } + +func TestOrigText(t *testing.T) { + strptr := func(str string) *string { + return &str + } + + pattern := BuildPattern(MODE_EXTENDED, CASE_SMART, []Range{}, nil, []rune("jg")) + for _, fun := range []func(*Chunk) []*Item{pattern.fuzzyMatch, pattern.extendedMatch} { + chunk := Chunk{ + &Item{text: strptr("junegunn"), origText: strptr("junegunn.choi")}, + } + matches := fun(&chunk) + if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" || + matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 { + t.Error("Invalid match result", matches) + } + } +} From ca4bdfb4bd61e1cb9991146ac5b6bafbf5391072 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 01:47:46 +0900 Subject: [PATCH 040/408] Fix Transform result cache to speed up subsequent searches --- src/item.go | 2 +- src/pattern.go | 22 ++++++++++++---------- src/pattern_test.go | 14 ++++++++++---- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/item.go b/src/item.go index 60355b4..9f90b8d 100644 --- a/src/item.go +++ b/src/item.go @@ -10,9 +10,9 @@ type Offset [2]int32 type Item struct { text *string origText *string + transformed *Transformed offsets []Offset rank Rank - transformed *Transformed } type Rank struct { diff --git a/src/pattern.go b/src/pattern.go index 93dbaf9..a936411 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -229,16 +229,22 @@ func (p *Pattern) Match(chunk *Chunk) []*Item { return matches } +func dupItem(item *Item, offsets []Offset) *Item { + return &Item{ + text: item.text, + origText: item.origText, + transformed: item.transformed, + offsets: offsets, + rank: Rank{0, 0, item.rank.index}} +} + func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { matches := []*Item{} for _, item := range *chunk { input := p.prepareInput(item) if sidx, eidx := p.iter(FuzzyMatch, input, p.text); sidx >= 0 { - matches = append(matches, &Item{ - text: item.text, - origText: item.origText, - offsets: []Offset{Offset{int32(sidx), int32(eidx)}}, - rank: Rank{0, 0, item.rank.index}}) + matches = append(matches, + dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}})) } } return matches @@ -262,11 +268,7 @@ func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { } } if len(offsets) == len(p.terms) { - matches = append(matches, &Item{ - text: item.text, - origText: item.origText, - offsets: offsets, - rank: Rank{0, 0, item.rank.index}}) + matches = append(matches, dupItem(item, offsets)) } } return matches diff --git a/src/pattern_test.go b/src/pattern_test.go index a776e30..2635b6c 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -86,19 +86,25 @@ func TestCaseSensitivity(t *testing.T) { } } -func TestOrigText(t *testing.T) { +func TestOrigTextAndTransformed(t *testing.T) { strptr := func(str string) *string { return &str } - pattern := BuildPattern(MODE_EXTENDED, CASE_SMART, []Range{}, nil, []rune("jg")) + tokens := Tokenize(strptr("junegunn"), nil) + trans := Transform(tokens, []Range{Range{1, 1}}) + for _, fun := range []func(*Chunk) []*Item{pattern.fuzzyMatch, pattern.extendedMatch} { chunk := Chunk{ - &Item{text: strptr("junegunn"), origText: strptr("junegunn.choi")}, + &Item{ + text: strptr("junegunn"), + origText: strptr("junegunn.choi"), + transformed: trans}, } matches := fun(&chunk) if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" || - matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 { + matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || + matches[0].transformed != trans { t.Error("Invalid match result", matches) } } From e293cd4d088ec1fc4b6a7b14a19825138a20c597 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 02:20:54 +0900 Subject: [PATCH 041/408] Add test cases for ChunkCache --- src/cache_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/cache_test.go diff --git a/src/cache_test.go b/src/cache_test.go new file mode 100644 index 0000000..2a8b048 --- /dev/null +++ b/src/cache_test.go @@ -0,0 +1,40 @@ +package fzf + +import "testing" + +func TestChunkCache(t *testing.T) { + cache := NewChunkCache() + chunk2 := make(Chunk, CHUNK_SIZE) + chunk1p := &Chunk{} + chunk2p := &chunk2 + items1 := []*Item{&Item{}} + items2 := []*Item{&Item{}, &Item{}} + cache.Add(chunk1p, "foo", items1) + cache.Add(chunk2p, "foo", items1) + cache.Add(chunk2p, "bar", items2) + + { // chunk1 is not full + cached, found := cache.Find(chunk1p, "foo") + if found { + t.Error("Cached disabled for non-empty chunks", found, cached) + } + } + { + cached, found := cache.Find(chunk2p, "foo") + if !found || len(cached) != 1 { + t.Error("Expected 1 item cached", found, cached) + } + } + { + cached, found := cache.Find(chunk2p, "bar") + if !found || len(cached) != 2 { + t.Error("Expected 2 items cached", found, cached) + } + } + { + cached, found := cache.Find(chunk1p, "foobar") + if found { + t.Error("Expected 0 item cached", found, cached) + } + } +} From bd7331ecf5f2dc6dd7b5e7e20979b8dc8021e04f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 03:45:49 +0900 Subject: [PATCH 042/408] Remove unnecessary loop label --- src/pattern.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index a936411..2aa45c2 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -255,12 +255,11 @@ func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { for _, item := range *chunk { input := p.prepareInput(item) offsets := []Offset{} - Loop: for _, term := range p.terms { pfun := p.procFun[term.typ] if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { if term.inv { - break Loop + break } offsets = append(offsets, Offset{int32(sidx), int32(eidx)}) } else if term.inv { From 313578a1a07e79aba273f47a281247e76e6329d6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 03:53:07 +0900 Subject: [PATCH 043/408] Improve prefix/suffix cache lookup --- src/pattern.go | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 2aa45c2..31ba813 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -194,24 +194,19 @@ func (p *Pattern) Match(chunk *Chunk) []*Item { } } - // ChunkCache: Prefix match - foundPrefixCache := false - for idx := len(cacheKey) - 1; idx > 0; idx-- { - if cached, found := _cache.Find(chunk, cacheKey[:idx]); found { - cachedChunk := Chunk(cached) - space = &cachedChunk - foundPrefixCache = true - break - } - } - - // ChunkCache: Suffix match - if !foundPrefixCache { - for idx := 1; idx < len(cacheKey); idx++ { - if cached, found := _cache.Find(chunk, cacheKey[idx:]); found { + // ChunkCache: Prefix/suffix match +Loop: + for idx := 1; idx < len(cacheKey); idx++ { + // [---------| ] | [ |---------] + // [--------| ] | [ |--------] + // [-------| ] | [ |-------] + prefix := cacheKey[:len(cacheKey)-idx] + suffix := cacheKey[idx:] + for _, substr := range [2]*string{&prefix, &suffix} { + if cached, found := _cache.Find(chunk, *substr); found { cachedChunk := Chunk(cached) space = &cachedChunk - break + break Loop } } } From 6c3489087c2067f42158b558c1d0a2217e1b16b3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 14:19:50 +0900 Subject: [PATCH 044/408] Refactor Makefile and Dockerfiles --- src/Dockerfile.arch | 6 +----- src/Dockerfile.centos | 6 +----- src/Dockerfile.ubuntu | 6 +----- src/Makefile | 40 ++++++++++++++++++++++++++-------------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch index 8f942db..054b95c 100644 --- a/src/Dockerfile.arch +++ b/src/Dockerfile.arch @@ -19,12 +19,8 @@ RUN echo '[multilib]' >> /etc/pacman.conf && \ pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \ cd $GOROOT/src && GOARCH=386 ./make.bash -# Symlink fzf directory -RUN mkdir -p /go/src/github.com/junegunn && \ - ln -s /fzf /go/src/github.com/junegunn/fzf - # Volume -VOLUME /fzf +VOLUME /go # Default CMD CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos index e791dc6..5b27925 100644 --- a/src/Dockerfile.centos +++ b/src/Dockerfile.centos @@ -13,12 +13,8 @@ ENV GOPATH /go ENV GOROOT /go1.4 ENV PATH /go1.4/bin:$PATH -# Symlink fzf directory -RUN mkdir -p /go/src/github.com/junegunn && \ - ln -s /fzf /go/src/github.com/junegunn/fzf - # Volume -VOLUME /fzf +VOLUME /go # Default CMD CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu index b7f6232..91bf780 100644 --- a/src/Dockerfile.ubuntu +++ b/src/Dockerfile.ubuntu @@ -18,12 +18,8 @@ ENV PATH /go1.4/bin:$PATH RUN apt-get install -y lib32ncurses5-dev && \ cd $GOROOT/src && GOARCH=386 ./make.bash -# Symlink fzf directory -RUN mkdir -p /go/src/github.com/junegunn && \ - ln -s /fzf /go/src/github.com/junegunn/fzf - # Volume -VOLUME /fzf +VOLUME /go # Default CMD CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash diff --git a/src/Makefile b/src/Makefile index 12767d3..7108f0d 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,3 +1,7 @@ +ifndef GOPATH +$(error GOPATH is undefined) +endif + UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) GOOS := darwin @@ -9,7 +13,7 @@ ifneq ($(shell uname -m),x86_64) $(error "Build on $(UNAME_M) is not supported, yet.") endif -SOURCES := $(wildcard *.go fzf/*.go) +SOURCES := $(wildcard *.go */*.go) BINDIR := ../bin BINARY32 := fzf-$(GOOS)_386 @@ -17,7 +21,7 @@ BINARY64 := fzf-$(GOOS)_amd64 RELEASE32 = fzf-$(shell fzf/$(BINARY64) --version)-$(GOOS)_386 RELEASE64 = fzf-$(shell fzf/$(BINARY64) --version)-$(GOOS)_amd64 -all: release +all: test release release: build cd fzf && \ @@ -27,23 +31,31 @@ release: build build: fzf/$(BINARY32) fzf/$(BINARY64) -test: $(SOURCES) +test: go get go test -v -fzf/$(BINARY32): test - cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32) +install: $(BINDIR)/fzf -fzf/$(BINARY64): test - cd fzf && go build -o $(BINARY64) - -install: fzf/$(BINARY64) - mkdir -p $(BINDIR) - cp -f fzf/$(BINARY64) $(BINDIR)/fzf +uninstall: + rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64) clean: cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz +fzf/$(BINARY32): $(SOURCES) + cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32) + +fzf/$(BINARY64): $(SOURCES) + cd fzf && go build -o $(BINARY64) + +$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) + cp -f fzf/$(BINARY64) $(BINDIR) + cd $(BINDIR) && ln -sf $(BINARY64) fzf + +$(BINDIR): + mkdir -p $@ + # Linux distribution to build fzf on DISTRO := arch @@ -51,11 +63,11 @@ docker: docker build -t junegunn/$(DISTRO)-sandbox - < Dockerfile.$(DISTRO) linux: docker - docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/$(DISTRO)-sandbox \ + docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make' $(DISTRO): docker - docker run -i -t -v $(shell cd ..; pwd):/fzf junegunn/$(DISTRO)-sandbox \ + docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' -.PHONY: build release install linux clean docker $(DISTRO) +.PHONY: all build release test install uninstall clean docker linux $(DISTRO) From 1c313526753c1ad48b11574883c70bd850af78d1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 16:59:57 +0900 Subject: [PATCH 045/408] Update src/README.md and package comment --- src/README.md | 65 +++++++++++++++++++++++++++++++++++---------------- src/core.go | 25 ++++++++++++++++++++ 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/src/README.md b/src/README.md index f179ae7..fb17e68 100644 --- a/src/README.md +++ b/src/README.md @@ -5,34 +5,44 @@ This directory contains the source code for the new fzf implementation in [Go][go]. The new version has the following benefits over the previous Ruby version. -- Immensely faster - - No GIL. Performance is linearly proportional to the number of cores. - - It's so fast that I even decided to remove the sort limit. `--sort=N` is - no longer required. -- Does not require Ruby and distributed as an executable binary - - Ruby dependency is especially painful on Ruby 2.1 or above which - ships without curses gem +Motivation +---------- -Build ------ +### No Ruby dependency -```sh -# Build fzf executables -make +There have always been complaints about fzf being a Ruby script. To make +matters worse, Ruby 2.1 dropped ncurses support from its standard libary. +Because of the change, users running Ruby 2.1 or above were forced to build C +extensions of curses gem to meet the requirement of fzf. The new Go version +will be distributed as an executable binary so it will be much more accessible +and easier to setup. -# Install the executable to ../bin directory -make install +### Performance -# Build executables for Linux using Docker -make linux -``` +With the presence of [GIL][gil], Ruby cannot utilize multiple CPU cores. Even +though the Ruby version of fzf was pretty responsive even for 100k+ lines, +which is well above the size of the usual input, it was obvious that we could +do better. Now with the Go version, GIL is gone, and the search performance +scales proportional to the number of cores. On my Macbook Pro (Mid 2012), it +was shown to be an order of magnitude faster on certain cases. It also starts +much faster than before though the difference shouldn't be really noticeable. + +Differences with Ruby version +----------------------------- + +The Go version is designed to be perfectly compatible with the previous Ruby +version. The only behavioral difference is that the new version ignores the +numeric argument to `--sort=N` option and always sorts the result regardless +of the number of matches. The value was introduced to limit the response time +of the query, but the Go version is blazingly fast (almost instant response +even for 1M+ items) so I decided that it's no longer required. System requirements ------------------- Currently prebuilt binaries are provided only for OS X and Linux. The install script will fall back to the legacy Ruby version on the other systems, but if -you have Go 1.4 installed, you can try building it yourself. (`make install`) +you have Go 1.4 installed, you can try building it yourself. However, as pointed out in [golang.org/doc/install][req], the Go version may not run on CentOS/RHEL 5.x and thus the install script will choose the Ruby @@ -44,6 +54,20 @@ impossible to support Windows by falling back to a cross-platform alternative such as [termbox][termbox] only on Windows. If you're interested in making fzf work on Windows, please let me know. +Build +----- + +```sh +# Build fzf executables and tarballs +make + +# Install the executable to ../bin directory +make install + +# Build executables and tarballs for Linux using Docker +make linux +``` + Third-party libraries used -------------------------- @@ -56,8 +80,8 @@ Third-party libraries used Contribution ------------ -For the moment, I will not add or accept any new features until we can be sure -that the implementation is stable and we have a sufficient number of test +For the time being, I will not add or accept any new features until we can be +sure that the implementation is stable and we have a sufficient number of test cases. However, fixes for obvious bugs and new test cases are welcome. I also care much about the performance of the implementation (that's the @@ -71,6 +95,7 @@ License [MIT](LICENSE) [go]: https://golang.org/ +[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock [ncurses]: https://www.gnu.org/software/ncurses/ [req]: http://golang.org/doc/install [termbox]: https://github.com/nsf/termbox-go diff --git a/src/core.go b/src/core.go index 98973f8..2970038 100644 --- a/src/core.go +++ b/src/core.go @@ -1,3 +1,28 @@ +/* +Package fzf implements fzf, a command-line fuzzy finder. + +The MIT License (MIT) + +Copyright (c) 2015 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ package fzf import ( From 1db68a3976cfb10ed7d6ab88d7b468bb1b93ee34 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 21:56:55 +0900 Subject: [PATCH 046/408] Avoid unnecessary update of search progress --- src/terminal.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 7039e57..7d8bc5b 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -94,9 +94,14 @@ func (t *Terminal) UpdateCount(cnt int, final bool) { func (t *Terminal) UpdateProgress(progress float32) { t.mutex.Lock() - t.progress = int(progress * 100) + newProgress := int(progress * 100) + changed := t.progress != newProgress + t.progress = newProgress t.mutex.Unlock() - t.reqBox.Set(REQ_INFO, nil) + + if changed { + t.reqBox.Set(REQ_INFO, nil) + } } func (t *Terminal) UpdateList(merger *Merger) { From 9dbf6b02d24b52ae43e36905bbb1e83087e1dfe9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Jan 2015 23:49:12 +0900 Subject: [PATCH 047/408] Fix race conditions - Wait for completions of goroutines when cancelling a search - Remove shared access to rank field of Item --- src/core.go | 6 +++++- src/item.go | 22 +++++++++++----------- src/item_test.go | 8 ++++---- src/matcher.go | 17 +++++++++++++---- src/merger.go | 2 +- src/merger_test.go | 12 +++++++++++- src/pattern.go | 5 ++++- 7 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/core.go b/src/core.go index 2970038..ab2a48f 100644 --- a/src/core.go +++ b/src/core.go @@ -64,7 +64,10 @@ func Run(options *Options) { var chunkList *ChunkList if len(opts.WithNth) == 0 { chunkList = NewChunkList(func(data *string, index int) *Item { - return &Item{text: data, rank: Rank{0, 0, uint32(index)}} + return &Item{ + text: data, + index: uint32(index), + rank: Rank{0, 0, uint32(index)}} }) } else { chunkList = NewChunkList(func(data *string, index int) *Item { @@ -72,6 +75,7 @@ func Run(options *Options) { item := Item{ text: Transform(tokens, opts.WithNth).whole, origText: data, + index: uint32(index), rank: Rank{0, 0, uint32(index)}} return &item }) diff --git a/src/item.go b/src/item.go index 9f90b8d..41aa34b 100644 --- a/src/item.go +++ b/src/item.go @@ -1,9 +1,6 @@ package fzf -import ( - "fmt" - "sort" -) +import "fmt" type Offset [2]int32 @@ -11,6 +8,7 @@ type Item struct { text *string origText *string transformed *Transformed + index uint32 offsets []Offset rank Rank } @@ -21,11 +19,10 @@ type Rank struct { index uint32 } -func (i *Item) Rank() Rank { - if i.rank.matchlen > 0 || i.rank.strlen > 0 { +func (i *Item) Rank(cache bool) Rank { + if cache && (i.rank.matchlen > 0 || i.rank.strlen > 0) { return i.rank } - sort.Sort(ByOrder(i.offsets)) matchlen := 0 prevEnd := 0 for _, offset := range i.offsets { @@ -41,8 +38,11 @@ func (i *Item) Rank() Rank { matchlen += end - begin } } - i.rank = Rank{uint16(matchlen), uint16(len(*i.text)), i.rank.index} - return i.rank + rank := Rank{uint16(matchlen), uint16(len(*i.text)), i.index} + if cache { + i.rank = rank + } + return rank } func (i *Item) Print() { @@ -80,8 +80,8 @@ func (a ByRelevance) Swap(i, j int) { } func (a ByRelevance) Less(i, j int) bool { - irank := a[i].Rank() - jrank := a[j].Rank() + irank := a[i].Rank(true) + jrank := a[j].Rank(true) return compareRanks(irank, jrank) } diff --git a/src/item_test.go b/src/item_test.go index 87d8be4..0e83631 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -31,13 +31,13 @@ func TestRankComparison(t *testing.T) { // Match length, string length, index func TestItemRank(t *testing.T) { strs := []string{"foo", "foobar", "bar", "baz"} - item1 := Item{text: &strs[0], rank: Rank{0, 0, 1}, offsets: []Offset{}} - rank1 := item1.Rank() + item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}} + rank1 := item1.Rank(true) if rank1.matchlen != 0 || rank1.strlen != 3 || rank1.index != 1 { - t.Error(item1.Rank()) + t.Error(item1.Rank(true)) } // Only differ in index - item2 := Item{text: &strs[0], rank: Rank{0, 0, 0}, offsets: []Offset{}} + item2 := Item{text: &strs[0], index: 0, offsets: []Offset{}} items := []*Item{&item1, &item2} sort.Sort(ByRelevance(items)) diff --git a/src/matcher.go b/src/matcher.go index 234e703..713b4dd 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -4,6 +4,7 @@ import ( "fmt" "runtime" "sort" + "sync" "time" ) @@ -134,10 +135,13 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { slices := m.sliceChunks(request.chunks) numSlices := len(slices) resultChan := make(chan partialResult, numSlices) - countChan := make(chan int, numSlices) + countChan := make(chan int, numChunks) + waitGroup := sync.WaitGroup{} for idx, chunks := range slices { + waitGroup.Add(1) go func(idx int, chunks []*Chunk) { + defer func() { waitGroup.Done() }() sliceMatches := []*Item{} for _, chunk := range chunks { var matches []*Item @@ -159,6 +163,12 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { }(idx, chunks) } + wait := func() bool { + cancelled.Set(true) + waitGroup.Wait() + return true + } + count := 0 matchCount := 0 for matchesInChunk := range countChan { @@ -166,7 +176,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { matchCount += matchesInChunk if limit > 0 && matchCount > limit { - return nil, true // For --select-1 and --exit-0 + return nil, wait() // For --select-1 and --exit-0 } if count == numChunks { @@ -174,8 +184,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { } if !empty && m.reqBox.Peak(REQ_RESET) { - cancelled.Set(true) - return nil, true + return nil, wait() } if time.Now().Sub(startedAt) > PROGRESS_MIN_DURATION { diff --git a/src/merger.go b/src/merger.go index 08a3d15..16afdaf 100644 --- a/src/merger.go +++ b/src/merger.go @@ -57,7 +57,7 @@ func (mg *Merger) mergedGet(idx int) *Item { continue } if cursor >= 0 { - rank := list[cursor].Rank() + rank := list[cursor].Rank(false) if minIdx < 0 || compareRanks(rank, minRank) { minRank = rank minIdx = listIdx diff --git a/src/merger_test.go b/src/merger_test.go index 32a1228..f79da09 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -1,6 +1,7 @@ package fzf import ( + "fmt" "math/rand" "sort" "testing" @@ -13,8 +14,17 @@ func assert(t *testing.T, cond bool, msg ...string) { } func randItem() *Item { + str := fmt.Sprintf("%d", rand.Uint32()) + offsets := make([]Offset, rand.Int()%3) + for idx := range offsets { + sidx := int32(rand.Uint32() % 20) + eidx := sidx + int32(rand.Uint32()%20) + offsets[idx] = Offset{sidx, eidx} + } return &Item{ - rank: Rank{uint16(rand.Uint32()), uint16(rand.Uint32()), rand.Uint32()}} + text: &str, + index: rand.Uint32(), + offsets: offsets} } func TestEmptyMerger(t *testing.T) { diff --git a/src/pattern.go b/src/pattern.go index 31ba813..2e7d6f9 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -2,6 +2,7 @@ package fzf import ( "regexp" + "sort" "strings" ) @@ -225,12 +226,14 @@ Loop: } func dupItem(item *Item, offsets []Offset) *Item { + sort.Sort(ByOrder(offsets)) return &Item{ text: item.text, origText: item.origText, transformed: item.transformed, + index: item.index, offsets: offsets, - rank: Rank{0, 0, item.rank.index}} + rank: Rank{0, 0, item.index}} } func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { From 7a2bc2cada971c7a390d09b0afda34780ff56fb6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 12 Jan 2015 03:01:24 +0900 Subject: [PATCH 048/408] Lint --- src/algo.go | 29 +++--- src/atomicbool.go | 5 + src/cache.go | 6 ++ src/cache_test.go | 2 +- src/chunklist.go | 20 +++- src/chunklist_test.go | 4 +- src/constants.go | 19 ++-- src/core.go | 43 +++++---- src/curses/curses.go | 218 +++++++++++++++++++++--------------------- src/eventbox.go | 11 ++- src/eventbox_test.go | 14 +-- src/item.go | 16 ++-- src/matcher.go | 31 +++--- src/merger.go | 10 +- src/options.go | 43 +++++---- src/options_test.go | 10 +- src/pattern.go | 77 ++++++++------- src/pattern_test.go | 54 +++++------ src/reader.go | 10 +- src/reader_test.go | 18 ++-- src/terminal.go | 188 ++++++++++++++++++------------------ src/tokenizer.go | 47 ++++----- src/tokenizer_test.go | 4 +- src/util.go | 4 + 24 files changed, 478 insertions(+), 405 deletions(-) diff --git a/src/algo.go b/src/algo.go index e0c173f..5f15ab3 100644 --- a/src/algo.go +++ b/src/algo.go @@ -10,6 +10,7 @@ import "strings" * In short: They try to do as little work as possible. */ +// FuzzyMatch performs fuzzy-match func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { runes := []rune(*input) @@ -36,7 +37,7 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { if sidx < 0 { sidx = index } - if pidx += 1; pidx == len(pattern) { + if pidx++; pidx == len(pattern) { eidx = index + 1 break } @@ -44,14 +45,14 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { } if sidx >= 0 && eidx >= 0 { - pidx -= 1 + pidx-- for index := eidx - 1; index >= sidx; index-- { char := runes[index] if !caseSensitive && char >= 65 && char <= 90 { char += 32 } if char == pattern[pidx] { - if pidx -= 1; pidx < 0 { + if pidx--; pidx < 0 { sidx = index break } @@ -62,6 +63,8 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { return -1, -1 } +// ExactMatchStrings performs exact-match using strings package. +// Currently not used. func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, int) { var str string if caseSensitive { @@ -77,15 +80,13 @@ func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, return -1, -1 } -/* - * This is a basic string searching algorithm that handles case sensitivity. - * Although naive, it still performs better than the combination of - * strings.ToLower + strings.Index for typical fzf use cases where input - * strings and patterns are not very long. - * - * We might try to implement better algorithms in the future: - * http://en.wikipedia.org/wiki/String_searching_algorithm - */ +// ExactMatchNaive is a basic string searching algorithm that handles case +// sensitivity. Although naive, it still performs better than the combination +// of strings.ToLower + strings.Index for typical fzf use cases where input +// strings and patterns are not very long. +// +// We might try to implement better algorithms in the future: +// http://en.wikipedia.org/wiki/String_searching_algorithm func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, int) { runes := []rune(*input) numRunes := len(runes) @@ -101,7 +102,7 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in char += 32 } if pattern[pidx] == char { - pidx += 1 + pidx++ if pidx == plen { return index - plen + 1, index + 1 } @@ -113,6 +114,7 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in return -1, -1 } +// PrefixMatch performs prefix-match func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { runes := []rune(*input) if len(runes) < len(pattern) { @@ -131,6 +133,7 @@ func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { return 0, len(pattern) } +// SuffixMatch performs suffix-match func SuffixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { runes := []rune(strings.TrimRight(*input, " ")) trimmedLen := len(runes) diff --git a/src/atomicbool.go b/src/atomicbool.go index f2f4894..b264724 100644 --- a/src/atomicbool.go +++ b/src/atomicbool.go @@ -2,23 +2,28 @@ package fzf import "sync" +// AtomicBool is a boxed-class that provides synchronized access to the +// underlying boolean value type AtomicBool struct { mutex sync.Mutex state bool } +// NewAtomicBool returns a new AtomicBool func NewAtomicBool(initialState bool) *AtomicBool { return &AtomicBool{ mutex: sync.Mutex{}, state: initialState} } +// Get returns the current boolean value synchronously func (a *AtomicBool) Get() bool { a.mutex.Lock() defer a.mutex.Unlock() return a.state } +// Set updates the boolean value synchronously func (a *AtomicBool) Set(newState bool) bool { a.mutex.Lock() defer a.mutex.Unlock() diff --git a/src/cache.go b/src/cache.go index 340f325..f2f84a0 100644 --- a/src/cache.go +++ b/src/cache.go @@ -2,16 +2,21 @@ package fzf import "sync" +// QueryCache associates strings to lists of items type QueryCache map[string][]*Item + +// ChunkCache associates Chunk and query string to lists of items type ChunkCache struct { mutex sync.Mutex cache map[*Chunk]*QueryCache } +// NewChunkCache returns a new ChunkCache func NewChunkCache() ChunkCache { return ChunkCache{sync.Mutex{}, make(map[*Chunk]*QueryCache)} } +// Add adds the list to the cache func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { if len(key) == 0 || !chunk.IsFull() { return @@ -28,6 +33,7 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { (*qc)[key] = list } +// Find is called to lookup ChunkCache func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) { if len(key) == 0 || !chunk.IsFull() { return nil, false diff --git a/src/cache_test.go b/src/cache_test.go index 2a8b048..3975eaa 100644 --- a/src/cache_test.go +++ b/src/cache_test.go @@ -4,7 +4,7 @@ import "testing" func TestChunkCache(t *testing.T) { cache := NewChunkCache() - chunk2 := make(Chunk, CHUNK_SIZE) + chunk2 := make(Chunk, ChunkSize) chunk1p := &Chunk{} chunk2p := &chunk2 items1 := []*Item{&Item{}} diff --git a/src/chunklist.go b/src/chunklist.go index 5bca6da..73983b1 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -2,12 +2,17 @@ package fzf import "sync" -const CHUNK_SIZE int = 100 +// Capacity of each chunk +const ChunkSize int = 100 +// Chunk is a list of Item pointers whose size has the upper limit of ChunkSize type Chunk []*Item // >>> []Item +// Transformer is a closure type that builds Item object from a pointer to a +// string and an integer type Transformer func(*string, int) *Item +// ChunkList is a list of Chunks type ChunkList struct { chunks []*Chunk count int @@ -15,6 +20,7 @@ type ChunkList struct { trans Transformer } +// NewChunkList returns a new ChunkList func NewChunkList(trans Transformer) *ChunkList { return &ChunkList{ chunks: []*Chunk{}, @@ -27,34 +33,38 @@ func (c *Chunk) push(trans Transformer, data *string, index int) { *c = append(*c, trans(data, index)) } +// IsFull returns true if the Chunk is full func (c *Chunk) IsFull() bool { - return len(*c) == CHUNK_SIZE + return len(*c) == ChunkSize } func (cl *ChunkList) lastChunk() *Chunk { return cl.chunks[len(cl.chunks)-1] } +// CountItems returns the total number of Items func CountItems(cs []*Chunk) int { if len(cs) == 0 { return 0 } - return CHUNK_SIZE*(len(cs)-1) + len(*(cs[len(cs)-1])) + return ChunkSize*(len(cs)-1) + len(*(cs[len(cs)-1])) } +// Push adds the item to the list func (cl *ChunkList) Push(data string) { cl.mutex.Lock() defer cl.mutex.Unlock() if len(cl.chunks) == 0 || cl.lastChunk().IsFull() { - newChunk := Chunk(make([]*Item, 0, CHUNK_SIZE)) + newChunk := Chunk(make([]*Item, 0, ChunkSize)) cl.chunks = append(cl.chunks, &newChunk) } cl.lastChunk().push(cl.trans, &data, cl.count) - cl.count += 1 + cl.count++ } +// Snapshot returns immutable snapshot of the ChunkList func (cl *ChunkList) Snapshot() ([]*Chunk, int) { cl.mutex.Lock() defer cl.mutex.Unlock() diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 09e4aad..02288d9 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -45,7 +45,7 @@ func TestChunkList(t *testing.T) { } // Add more data - for i := 0; i < CHUNK_SIZE*2; i++ { + for i := 0; i < ChunkSize*2; i++ { cl.Push(fmt.Sprintf("item %d", i)) } @@ -57,7 +57,7 @@ func TestChunkList(t *testing.T) { // New snapshot snapshot, count = cl.Snapshot() if len(snapshot) != 3 || !snapshot[0].IsFull() || - !snapshot[1].IsFull() || snapshot[2].IsFull() || count != CHUNK_SIZE*2+2 { + !snapshot[1].IsFull() || snapshot[2].IsFull() || count != ChunkSize*2+2 { t.Error("Expected two full chunks and one more chunk") } if len(*snapshot[2]) != 2 { diff --git a/src/constants.go b/src/constants.go index b0b64db..80eb634 100644 --- a/src/constants.go +++ b/src/constants.go @@ -1,12 +1,17 @@ package fzf -const VERSION = "0.9.0" +// Current version +const Version = "0.9.0" +// EventType is the type for fzf events +type EventType int + +// fzf events const ( - EVT_READ_NEW EventType = iota - EVT_READ_FIN - EVT_SEARCH_NEW - EVT_SEARCH_PROGRESS - EVT_SEARCH_FIN - EVT_CLOSE + EvtReadNew EventType = iota + EvtReadFin + EvtSearchNew + EvtSearchProgress + EvtSearchFin + EvtClose ) diff --git a/src/core.go b/src/core.go index ab2a48f..65e641c 100644 --- a/src/core.go +++ b/src/core.go @@ -32,28 +32,29 @@ import ( "time" ) -const COORDINATOR_DELAY_MAX time.Duration = 100 * time.Millisecond -const COORDINATOR_DELAY_STEP time.Duration = 10 * time.Millisecond +const coordinatorDelayMax time.Duration = 100 * time.Millisecond +const coordinatorDelayStep time.Duration = 10 * time.Millisecond func initProcs() { runtime.GOMAXPROCS(runtime.NumCPU()) } /* -Reader -> EVT_READ_FIN -Reader -> EVT_READ_NEW -> Matcher (restart) -Terminal -> EVT_SEARCH_NEW -> Matcher (restart) -Matcher -> EVT_SEARCH_PROGRESS -> Terminal (update info) -Matcher -> EVT_SEARCH_FIN -> Terminal (update list) +Reader -> EvtReadFin +Reader -> EvtReadNew -> Matcher (restart) +Terminal -> EvtSearchNew -> Matcher (restart) +Matcher -> EvtSearchProgress -> Terminal (update info) +Matcher -> EvtSearchFin -> Terminal (update list) */ +// Run starts fzf func Run(options *Options) { initProcs() opts := ParseOptions() if opts.Version { - fmt.Println(VERSION) + fmt.Println(Version) os.Exit(0) } @@ -108,12 +109,12 @@ func Run(options *Options) { pattern := patternBuilder([]rune(patternString)) looping := true - eventBox.Unwatch(EVT_READ_NEW) + eventBox.Unwatch(EvtReadNew) for looping { eventBox.Wait(func(events *Events) { - for evt, _ := range *events { + for evt := range *events { switch evt { - case EVT_READ_FIN: + case EvtReadFin: looping = false return } @@ -133,7 +134,7 @@ func Run(options *Options) { fmt.Println(patternString) } for i := 0; i < merger.Length(); i++ { - merger.Get(i).Print() + fmt.Println(merger.Get(i).AsString()) } os.Exit(0) } @@ -149,33 +150,33 @@ func Run(options *Options) { // Event coordination reading := true ticks := 0 - eventBox.Watch(EVT_READ_NEW) + eventBox.Watch(EvtReadNew) for { delay := true - ticks += 1 + ticks++ eventBox.Wait(func(events *Events) { defer events.Clear() for evt, value := range *events { switch evt { - case EVT_READ_NEW, EVT_READ_FIN: - reading = reading && evt == EVT_READ_NEW + case EvtReadNew, EvtReadFin: + reading = reading && evt == EvtReadNew snapshot, count := chunkList.Snapshot() terminal.UpdateCount(count, !reading) matcher.Reset(snapshot, terminal.Input(), false) - case EVT_SEARCH_NEW: + case EvtSearchNew: snapshot, _ := chunkList.Snapshot() matcher.Reset(snapshot, terminal.Input(), true) delay = false - case EVT_SEARCH_PROGRESS: + case EvtSearchProgress: switch val := value.(type) { case float32: terminal.UpdateProgress(val) } - case EVT_SEARCH_FIN: + case EvtSearchFin: switch val := value.(type) { case *Merger: terminal.UpdateList(val) @@ -185,8 +186,8 @@ func Run(options *Options) { }) if delay && reading { dur := DurWithin( - time.Duration(ticks)*COORDINATOR_DELAY_STEP, - 0, COORDINATOR_DELAY_MAX) + time.Duration(ticks)*coordinatorDelayStep, + 0, coordinatorDelayMax) time.Sleep(dur) } } diff --git a/src/curses/curses.go b/src/curses/curses.go index 736ccf6..8ebb583 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -20,66 +20,68 @@ import ( "unicode/utf8" ) +// Types of user action const ( - RUNE = iota + Rune = iota - CTRL_A - CTRL_B - CTRL_C - CTRL_D - CTRL_E - CTRL_F - CTRL_G - CTRL_H - TAB - CTRL_J - CTRL_K - CTRL_L - CTRL_M - CTRL_N - CTRL_O - CTRL_P - CTRL_Q - CTRL_R - CTRL_S - CTRL_T - CTRL_U - CTRL_V - CTRL_W - CTRL_X - CTRL_Y - CTRL_Z + CtrlA + CtrlB + CtrlC + CtrlD + CtrlE + CtrlF + CtrlG + CtrlH + Tab + CtrlJ + CtrlK + CtrlL + CtrlM + CtrlN + CtrlO + CtrlP + CtrlQ + CtrlR + CtrlS + CtrlT + CtrlU + CtrlV + CtrlW + CtrlX + CtrlY + CtrlZ ESC - INVALID - MOUSE + Invalid + Mouse - BTAB + BTab - DEL - PGUP - PGDN + Del + PgUp + PgDn - ALT_B - ALT_F - ALT_D - ALT_BS + AltB + AltF + AltD + AltBS +) + +// Pallete +const ( + ColNormal = iota + ColPrompt + ColMatch + ColCurrent + ColCurrentMatch + ColSpinner + ColInfo + ColCursor + ColSelected ) const ( - COL_NORMAL = iota - COL_PROMPT - COL_MATCH - COL_CURRENT - COL_CURRENT_MATCH - COL_SPINNER - COL_INFO - COL_CURSOR - COL_SELECTED -) - -const ( - DOUBLE_CLICK_DURATION = 500 * time.Millisecond + doubleClickDuration = 500 * time.Millisecond ) type Event struct { @@ -112,8 +114,8 @@ func init() { } func attrColored(pair int, bold bool) C.int { - var attr C.int = 0 - if pair > COL_NORMAL { + var attr C.int + if pair > ColNormal { attr = C.COLOR_PAIR(C.int(pair)) } if bold { @@ -123,15 +125,15 @@ func attrColored(pair int, bold bool) C.int { } func attrMono(pair int, bold bool) C.int { - var attr C.int = 0 + var attr C.int switch pair { - case COL_CURRENT: + case ColCurrent: if bold { attr = C.A_REVERSE } - case COL_MATCH: + case ColMatch: attr = C.A_UNDERLINE - case COL_CURRENT_MATCH: + case ColCurrentMatch: attr = C.A_UNDERLINE | C.A_REVERSE } if bold { @@ -198,23 +200,23 @@ func Init(color bool, color256 bool, black bool, mouse bool) { bg = -1 } if color256 { - C.init_pair(COL_PROMPT, 110, bg) - C.init_pair(COL_MATCH, 108, bg) - C.init_pair(COL_CURRENT, 254, 236) - C.init_pair(COL_CURRENT_MATCH, 151, 236) - C.init_pair(COL_SPINNER, 148, bg) - C.init_pair(COL_INFO, 144, bg) - C.init_pair(COL_CURSOR, 161, 236) - C.init_pair(COL_SELECTED, 168, 236) + C.init_pair(ColPrompt, 110, bg) + C.init_pair(ColMatch, 108, bg) + C.init_pair(ColCurrent, 254, 236) + C.init_pair(ColCurrentMatch, 151, 236) + C.init_pair(ColSpinner, 148, bg) + C.init_pair(ColInfo, 144, bg) + C.init_pair(ColCursor, 161, 236) + C.init_pair(ColSelected, 168, 236) } else { - C.init_pair(COL_PROMPT, C.COLOR_BLUE, bg) - C.init_pair(COL_MATCH, C.COLOR_GREEN, bg) - C.init_pair(COL_CURRENT, C.COLOR_YELLOW, C.COLOR_BLACK) - C.init_pair(COL_CURRENT_MATCH, C.COLOR_GREEN, C.COLOR_BLACK) - C.init_pair(COL_SPINNER, C.COLOR_GREEN, bg) - C.init_pair(COL_INFO, C.COLOR_WHITE, bg) - C.init_pair(COL_CURSOR, C.COLOR_RED, C.COLOR_BLACK) - C.init_pair(COL_SELECTED, C.COLOR_MAGENTA, C.COLOR_BLACK) + C.init_pair(ColPrompt, C.COLOR_BLUE, bg) + C.init_pair(ColMatch, C.COLOR_GREEN, bg) + C.init_pair(ColCurrent, C.COLOR_YELLOW, C.COLOR_BLACK) + C.init_pair(ColCurrentMatch, C.COLOR_GREEN, C.COLOR_BLACK) + C.init_pair(ColSpinner, C.COLOR_GREEN, bg) + C.init_pair(ColInfo, C.COLOR_WHITE, bg) + C.init_pair(ColCursor, C.COLOR_RED, C.COLOR_BLACK) + C.init_pair(ColSelected, C.COLOR_MAGENTA, C.COLOR_BLACK) } _color = attrColored } else { @@ -245,7 +247,7 @@ func GetBytes() []byte { // 27 (91 79) 77 type x y func mouseSequence(sz *int) Event { if len(_buf) < 6 { - return Event{INVALID, 0, nil} + return Event{Invalid, 0, nil} } *sz = 6 switch _buf[3] { @@ -258,7 +260,7 @@ func mouseSequence(sz *int) Event { double := false if down { now := time.Now() - if now.Sub(_prevDownTime) < DOUBLE_CLICK_DURATION { + if now.Sub(_prevDownTime) < doubleClickDuration { _clickY = append(_clickY, y) } else { _clickY = []int{y} @@ -266,18 +268,18 @@ func mouseSequence(sz *int) Event { _prevDownTime = now } else { if len(_clickY) > 1 && _clickY[0] == _clickY[1] && - time.Now().Sub(_prevDownTime) < DOUBLE_CLICK_DURATION { + time.Now().Sub(_prevDownTime) < doubleClickDuration { double = true } } - return Event{MOUSE, 0, &MouseEvent{y, x, 0, down, double, mod}} + return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl mod := _buf[3] >= 100 s := 1 - int(_buf[3]%2)*2 - return Event{MOUSE, 0, &MouseEvent{0, 0, s, false, false, mod}} + return Event{Mouse, 0, &MouseEvent{0, 0, s, false, false, mod}} } - return Event{INVALID, 0, nil} + return Event{Invalid, 0, nil} } func escSequence(sz *int) Event { @@ -287,81 +289,81 @@ func escSequence(sz *int) Event { *sz = 2 switch _buf[1] { case 98: - return Event{ALT_B, 0, nil} + return Event{AltB, 0, nil} case 100: - return Event{ALT_D, 0, nil} + return Event{AltD, 0, nil} case 102: - return Event{ALT_F, 0, nil} + return Event{AltF, 0, nil} case 127: - return Event{ALT_BS, 0, nil} + return Event{AltBS, 0, nil} case 91, 79: if len(_buf) < 3 { - return Event{INVALID, 0, nil} + return Event{Invalid, 0, nil} } *sz = 3 switch _buf[2] { case 68: - return Event{CTRL_B, 0, nil} + return Event{CtrlB, 0, nil} case 67: - return Event{CTRL_F, 0, nil} + return Event{CtrlF, 0, nil} case 66: - return Event{CTRL_J, 0, nil} + return Event{CtrlJ, 0, nil} case 65: - return Event{CTRL_K, 0, nil} + return Event{CtrlK, 0, nil} case 90: - return Event{BTAB, 0, nil} + return Event{BTab, 0, nil} case 72: - return Event{CTRL_A, 0, nil} + return Event{CtrlA, 0, nil} case 70: - return Event{CTRL_E, 0, nil} + return Event{CtrlE, 0, nil} case 77: return mouseSequence(sz) case 49, 50, 51, 52, 53, 54: if len(_buf) < 4 { - return Event{INVALID, 0, nil} + return Event{Invalid, 0, nil} } *sz = 4 switch _buf[2] { case 50: - return Event{INVALID, 0, nil} // INS + return Event{Invalid, 0, nil} // INS case 51: - return Event{DEL, 0, nil} + return Event{Del, 0, nil} case 52: - return Event{CTRL_E, 0, nil} + return Event{CtrlE, 0, nil} case 53: - return Event{PGUP, 0, nil} + return Event{PgUp, 0, nil} case 54: - return Event{PGDN, 0, nil} + return Event{PgDn, 0, nil} case 49: switch _buf[3] { case 126: - return Event{CTRL_A, 0, nil} + return Event{CtrlA, 0, nil} case 59: if len(_buf) != 6 { - return Event{INVALID, 0, nil} + return Event{Invalid, 0, nil} } *sz = 6 switch _buf[4] { case 50: switch _buf[5] { case 68: - return Event{CTRL_A, 0, nil} + return Event{CtrlA, 0, nil} case 67: - return Event{CTRL_E, 0, nil} + return Event{CtrlE, 0, nil} } case 53: switch _buf[5] { case 68: - return Event{ALT_B, 0, nil} + return Event{AltB, 0, nil} case 67: - return Event{ALT_F, 0, nil} + return Event{AltF, 0, nil} } } // _buf[4] } // _buf[3] } // _buf[2] } // _buf[2] } // _buf[1] - return Event{INVALID, 0, nil} + return Event{Invalid, 0, nil} } func GetChar() Event { @@ -378,21 +380,21 @@ func GetChar() Event { }() switch _buf[0] { - case CTRL_C, CTRL_G, CTRL_Q: - return Event{CTRL_C, 0, nil} + case CtrlC, CtrlG, CtrlQ: + return Event{CtrlC, 0, nil} case 127: - return Event{CTRL_H, 0, nil} + return Event{CtrlH, 0, nil} case ESC: return escSequence(&sz) } // CTRL-A ~ CTRL-Z - if _buf[0] <= CTRL_Z { + if _buf[0] <= CtrlZ { return Event{int(_buf[0]), 0, nil} } r, rsz := utf8.DecodeRune(_buf) sz = rsz - return Event{RUNE, r, nil} + return Event{Rune, r, nil} } func Move(y int, x int) { diff --git a/src/eventbox.go b/src/eventbox.go index 95126cc..0c8f922 100644 --- a/src/eventbox.go +++ b/src/eventbox.go @@ -2,16 +2,17 @@ package fzf import "sync" -type EventType int - +// Events is a type that associates EventType to any data type Events map[EventType]interface{} +// EventBox is used for coordinating events type EventBox struct { events Events cond *sync.Cond ignore map[EventType]bool } +// NewEventBox returns a new EventBox func NewEventBox() *EventBox { return &EventBox{ events: make(Events), @@ -19,6 +20,7 @@ func NewEventBox() *EventBox { ignore: make(map[EventType]bool)} } +// Wait blocks the goroutine until signaled func (b *EventBox) Wait(callback func(*Events)) { b.cond.L.Lock() defer b.cond.L.Unlock() @@ -30,6 +32,7 @@ func (b *EventBox) Wait(callback func(*Events)) { callback(&b.events) } +// Set turns on the event type on the box func (b *EventBox) Set(event EventType, value interface{}) { b.cond.L.Lock() defer b.cond.L.Unlock() @@ -39,6 +42,7 @@ func (b *EventBox) Set(event EventType, value interface{}) { } } +// Clear clears the events // Unsynchronized; should be called within Wait routine func (events *Events) Clear() { for event := range *events { @@ -46,6 +50,7 @@ func (events *Events) Clear() { } } +// Peak peaks at the event box if the given event is set func (b *EventBox) Peak(event EventType) bool { b.cond.L.Lock() defer b.cond.L.Unlock() @@ -53,6 +58,7 @@ func (b *EventBox) Peak(event EventType) bool { return ok } +// Watch deletes the events from the ignore list func (b *EventBox) Watch(events ...EventType) { b.cond.L.Lock() defer b.cond.L.Unlock() @@ -61,6 +67,7 @@ func (b *EventBox) Watch(events ...EventType) { } } +// Unwatch adds the events to the ignore list func (b *EventBox) Unwatch(events ...EventType) { b.cond.L.Lock() defer b.cond.L.Unlock() diff --git a/src/eventbox_test.go b/src/eventbox_test.go index fb0ceed..1cd7f22 100644 --- a/src/eventbox_test.go +++ b/src/eventbox_test.go @@ -9,16 +9,16 @@ func TestEventBox(t *testing.T) { ch := make(chan bool) go func() { - eb.Set(EVT_READ_NEW, 10) + eb.Set(EvtReadNew, 10) ch <- true <-ch - eb.Set(EVT_SEARCH_NEW, 10) - eb.Set(EVT_SEARCH_NEW, 15) - eb.Set(EVT_SEARCH_NEW, 20) - eb.Set(EVT_SEARCH_PROGRESS, 30) + eb.Set(EvtSearchNew, 10) + eb.Set(EvtSearchNew, 15) + eb.Set(EvtSearchNew, 20) + eb.Set(EvtSearchProgress, 30) ch <- true <-ch - eb.Set(EVT_SEARCH_FIN, 40) + eb.Set(EvtSearchFin, 40) ch <- true <-ch }() @@ -39,7 +39,7 @@ func TestEventBox(t *testing.T) { events.Clear() }) ch <- true - count += 1 + count++ } if count != 3 { diff --git a/src/item.go b/src/item.go index 41aa34b..4cbd3f9 100644 --- a/src/item.go +++ b/src/item.go @@ -1,9 +1,9 @@ package fzf -import "fmt" - +// Offset holds two 32-bit integers denoting the offsets of a matched substring type Offset [2]int32 +// Item represents each input line type Item struct { text *string origText *string @@ -13,12 +13,14 @@ type Item struct { rank Rank } +// Rank is used to sort the search result type Rank struct { matchlen uint16 strlen uint16 index uint32 } +// Rank calculates rank of the Item func (i *Item) Rank(cache bool) Rank { if cache && (i.rank.matchlen > 0 || i.rank.strlen > 0) { return i.rank @@ -45,14 +47,15 @@ func (i *Item) Rank(cache bool) Rank { return rank } -func (i *Item) Print() { +// AsString returns the original string +func (i *Item) AsString() string { if i.origText != nil { - fmt.Println(*i.origText) - } else { - fmt.Println(*i.text) + return *i.origText } + return *i.text } +// ByOrder is for sorting substring offsets type ByOrder []Offset func (a ByOrder) Len() int { @@ -69,6 +72,7 @@ func (a ByOrder) Less(i, j int) bool { return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1]) } +// ByRelevance is for sorting Items type ByRelevance []*Item func (a ByRelevance) Len() int { diff --git a/src/matcher.go b/src/matcher.go index 713b4dd..b8be287 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -8,11 +8,13 @@ import ( "time" ) +// MatchRequest represents a search request type MatchRequest struct { chunks []*Chunk pattern *Pattern } +// Matcher is responsible for performing search type Matcher struct { patternBuilder func([]rune) *Pattern sort bool @@ -23,20 +25,15 @@ type Matcher struct { } const ( - REQ_RETRY EventType = iota - REQ_RESET + reqRetry EventType = iota + reqReset ) const ( - STAT_CANCELLED int = iota - STAT_QCH - STAT_CHUNKS -) - -const ( - PROGRESS_MIN_DURATION = 200 * time.Millisecond + progressMinDuration = 200 * time.Millisecond ) +// NewMatcher returns a new Matcher func NewMatcher(patternBuilder func([]rune) *Pattern, sort bool, eventBox *EventBox) *Matcher { return &Matcher{ @@ -48,6 +45,7 @@ func NewMatcher(patternBuilder func([]rune) *Pattern, mergerCache: make(map[string]*Merger)} } +// Loop puts Matcher in action func (m *Matcher) Loop() { prevCount := 0 @@ -91,7 +89,7 @@ func (m *Matcher) Loop() { if !cancelled { m.mergerCache[patternString] = merger - m.eventBox.Set(EVT_SEARCH_FIN, merger) + m.eventBox.Set(EvtSearchFin, merger) } } } @@ -172,7 +170,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { count := 0 matchCount := 0 for matchesInChunk := range countChan { - count += 1 + count++ matchCount += matchesInChunk if limit > 0 && matchCount > limit { @@ -183,12 +181,12 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { break } - if !empty && m.reqBox.Peak(REQ_RESET) { + if !empty && m.reqBox.Peak(reqReset) { return nil, wait() } - if time.Now().Sub(startedAt) > PROGRESS_MIN_DURATION { - m.eventBox.Set(EVT_SEARCH_PROGRESS, float32(count)/float32(numChunks)) + if time.Now().Sub(startedAt) > progressMinDuration { + m.eventBox.Set(EvtSearchProgress, float32(count)/float32(numChunks)) } } @@ -200,14 +198,15 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { return NewMerger(partialResults, !empty && m.sort), false } +// Reset is called to interrupt/signal the ongoing search func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { pattern := m.patternBuilder(patternRunes) var event EventType if cancel { - event = REQ_RESET + event = reqReset } else { - event = REQ_RETRY + event = reqRetry } m.reqBox.Set(event, MatchRequest{chunks, pattern}) } diff --git a/src/merger.go b/src/merger.go index 16afdaf..bd2158d 100644 --- a/src/merger.go +++ b/src/merger.go @@ -2,8 +2,11 @@ package fzf import "fmt" -var EmptyMerger *Merger = NewMerger([][]*Item{}, false) +// Merger with no data +var EmptyMerger = NewMerger([][]*Item{}, false) +// Merger holds a set of locally sorted lists of items and provides the view of +// a single, globally-sorted list type Merger struct { lists [][]*Item merged []*Item @@ -12,6 +15,7 @@ type Merger struct { count int } +// NewMerger returns a new Merger func NewMerger(lists [][]*Item, sorted bool) *Merger { mg := Merger{ lists: lists, @@ -26,10 +30,12 @@ func NewMerger(lists [][]*Item, sorted bool) *Merger { return &mg } +// Length returns the number of items func (mg *Merger) Length() int { return mg.count } +// Get returns the pointer to the Item object indexed by the given integer func (mg *Merger) Get(idx int) *Item { if len(mg.lists) == 1 { return mg.lists[0][idx] @@ -69,7 +75,7 @@ func (mg *Merger) mergedGet(idx int) *Item { if minIdx >= 0 { chosen := mg.lists[minIdx] mg.merged = append(mg.merged, chosen[mg.cursors[minIdx]]) - mg.cursors[minIdx] += 1 + mg.cursors[minIdx]++ } else { panic(fmt.Sprintf("Index out of bounds (sorted, %d/%d)", i, mg.count)) } diff --git a/src/options.go b/src/options.go index 4929dfd..cf0608b 100644 --- a/src/options.go +++ b/src/options.go @@ -8,7 +8,7 @@ import ( "strings" ) -const USAGE = `usage: fzf [options] +const usage = `usage: fzf [options] Search -x, --extended Extended-search mode @@ -47,22 +47,27 @@ const USAGE = `usage: fzf [options] ` +// Mode denotes the current search mode type Mode int +// Search modes const ( - MODE_FUZZY Mode = iota - MODE_EXTENDED - MODE_EXTENDED_EXACT + ModeFuzzy Mode = iota + ModeExtended + ModeExtendedExact ) +// Case denotes case-sensitivity of search type Case int +// Case-sensitivities const ( - CASE_SMART Case = iota - CASE_IGNORE - CASE_RESPECT + CaseSmart Case = iota + CaseIgnore + CaseRespect ) +// Options stores the values of command-line options type Options struct { Mode Mode Case Case @@ -85,10 +90,10 @@ type Options struct { Version bool } -func DefaultOptions() *Options { +func defaultOptions() *Options { return &Options{ - Mode: MODE_FUZZY, - Case: CASE_SMART, + Mode: ModeFuzzy, + Case: CaseSmart, Nth: make([]Range, 0), WithNth: make([]Range, 0), Delimiter: nil, @@ -109,7 +114,7 @@ func DefaultOptions() *Options { } func help(ok int) { - os.Stderr.WriteString(USAGE) + os.Stderr.WriteString(usage) os.Exit(ok) } @@ -123,9 +128,8 @@ func optString(arg string, prefix string) (bool, string) { matches := rx.FindStringSubmatch(arg) if len(matches) > 1 { return true, matches[1] - } else { - return false, "" } + return false, "" } func nextString(args []string, i *int, message string) string { @@ -183,11 +187,11 @@ func parseOptions(opts *Options, allArgs []string) { case "-h", "--help": help(0) case "-x", "--extended": - opts.Mode = MODE_EXTENDED + opts.Mode = ModeExtended case "-e", "--extended-exact": - opts.Mode = MODE_EXTENDED_EXACT + opts.Mode = ModeExtendedExact case "+x", "--no-extended", "+e", "--no-extended-exact": - opts.Mode = MODE_FUZZY + opts.Mode = ModeFuzzy case "-q", "--query": opts.Query = nextString(allArgs, &i, "query string required") case "-f", "--filter": @@ -204,9 +208,9 @@ func parseOptions(opts *Options, allArgs []string) { case "+s", "--no-sort": opts.Sort = 0 case "-i": - opts.Case = CASE_IGNORE + opts.Case = CaseIgnore case "+i": - opts.Case = CASE_RESPECT + opts.Case = CaseRespect case "-m", "--multi": opts.Multi = true case "+m", "--no-multi": @@ -263,8 +267,9 @@ func parseOptions(opts *Options, allArgs []string) { } } +// ParseOptions parses command-line options func ParseOptions() *Options { - opts := DefaultOptions() + opts := defaultOptions() // Options from Env var words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS")) diff --git a/src/options_test.go b/src/options_test.go index f0aa3a0..e10ec56 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -15,20 +15,20 @@ func TestSplitNth(t *testing.T) { { ranges := splitNth("..") if len(ranges) != 1 || - ranges[0].begin != RANGE_ELLIPSIS || - ranges[0].end != RANGE_ELLIPSIS { + ranges[0].begin != rangeEllipsis || + ranges[0].end != rangeEllipsis { t.Errorf("%s", ranges) } } { ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2") if len(ranges) != 8 || - ranges[0].begin != RANGE_ELLIPSIS || ranges[0].end != 3 || - ranges[1].begin != 1 || ranges[1].end != RANGE_ELLIPSIS || + ranges[0].begin != rangeEllipsis || ranges[0].end != 3 || + ranges[1].begin != 1 || ranges[1].end != rangeEllipsis || ranges[2].begin != 2 || ranges[2].end != 3 || ranges[3].begin != 4 || ranges[3].end != -1 || ranges[4].begin != -3 || ranges[4].end != -2 || - ranges[5].begin != RANGE_ELLIPSIS || ranges[5].end != RANGE_ELLIPSIS || + ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis || ranges[6].begin != 2 || ranges[6].end != 2 || ranges[7].begin != -2 || ranges[7].end != -2 { t.Errorf("%s", ranges) diff --git a/src/pattern.go b/src/pattern.go index 2e7d6f9..9f32de6 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -6,7 +6,7 @@ import ( "strings" ) -const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +const uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" // fuzzy // 'exact @@ -17,31 +17,32 @@ const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" // !^not-exact-prefix // !not-exact-suffix$ -type TermType int +type termType int const ( - TERM_FUZZY TermType = iota - TERM_EXACT - TERM_PREFIX - TERM_SUFFIX + termFuzzy termType = iota + termExact + termPrefix + termSuffix ) -type Term struct { - typ TermType +type term struct { + typ termType inv bool text []rune origText []rune } +// Pattern represents search pattern type Pattern struct { mode Mode caseSensitive bool text []rune - terms []Term + terms []term hasInvTerm bool delimiter *regexp.Regexp nth []Range - procFun map[TermType]func(bool, *string, []rune) (int, int) + procFun map[termType]func(bool, *string, []rune) (int, int) } var ( @@ -62,12 +63,13 @@ func clearPatternCache() { _patternCache = make(map[string]*Pattern) } +// BuildPattern builds Pattern object from the given arguments func BuildPattern(mode Mode, caseMode Case, nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern { var asString string switch mode { - case MODE_EXTENDED, MODE_EXTENDED_EXACT: + case ModeExtended, ModeExtendedExact: asString = strings.Trim(string(runes), " ") default: asString = string(runes) @@ -79,19 +81,19 @@ func BuildPattern(mode Mode, caseMode Case, } caseSensitive, hasInvTerm := true, false - terms := []Term{} + terms := []term{} switch caseMode { - case CASE_SMART: - if !strings.ContainsAny(asString, UPPERCASE) { + case CaseSmart: + if !strings.ContainsAny(asString, uppercaseLetters) { runes, caseSensitive = []rune(strings.ToLower(asString)), false } - case CASE_IGNORE: + case CaseIgnore: runes, caseSensitive = []rune(strings.ToLower(asString)), false } switch mode { - case MODE_EXTENDED, MODE_EXTENDED_EXACT: + case ModeExtended, ModeExtendedExact: terms = parseTerms(mode, string(runes)) for _, term := range terms { if term.inv { @@ -108,25 +110,25 @@ func BuildPattern(mode Mode, caseMode Case, hasInvTerm: hasInvTerm, nth: nth, delimiter: delimiter, - procFun: make(map[TermType]func(bool, *string, []rune) (int, int))} + procFun: make(map[termType]func(bool, *string, []rune) (int, int))} - ptr.procFun[TERM_FUZZY] = FuzzyMatch - ptr.procFun[TERM_EXACT] = ExactMatchNaive - ptr.procFun[TERM_PREFIX] = PrefixMatch - ptr.procFun[TERM_SUFFIX] = SuffixMatch + ptr.procFun[termFuzzy] = FuzzyMatch + ptr.procFun[termExact] = ExactMatchNaive + ptr.procFun[termPrefix] = PrefixMatch + ptr.procFun[termSuffix] = SuffixMatch _patternCache[asString] = ptr return ptr } -func parseTerms(mode Mode, str string) []Term { +func parseTerms(mode Mode, str string) []term { tokens := _splitRegex.Split(str, -1) - terms := []Term{} + terms := []term{} for _, token := range tokens { - typ, inv, text := TERM_FUZZY, false, token + typ, inv, text := termFuzzy, false, token origText := []rune(text) - if mode == MODE_EXTENDED_EXACT { - typ = TERM_EXACT + if mode == ModeExtendedExact { + typ = termExact } if strings.HasPrefix(text, "!") { @@ -135,20 +137,20 @@ func parseTerms(mode Mode, str string) []Term { } if strings.HasPrefix(text, "'") { - if mode == MODE_EXTENDED { - typ = TERM_EXACT + if mode == ModeExtended { + typ = termExact text = text[1:] } } else if strings.HasPrefix(text, "^") { - typ = TERM_PREFIX + typ = termPrefix text = text[1:] } else if strings.HasSuffix(text, "$") { - typ = TERM_SUFFIX + typ = termSuffix text = text[:len(text)-1] } if len(text) > 0 { - terms = append(terms, Term{ + terms = append(terms, term{ typ: typ, inv: inv, text: []rune(text), @@ -158,20 +160,22 @@ func parseTerms(mode Mode, str string) []Term { return terms } +// IsEmpty returns true if the pattern is effectively empty func (p *Pattern) IsEmpty() bool { - if p.mode == MODE_FUZZY { + if p.mode == ModeFuzzy { return len(p.text) == 0 - } else { - return len(p.terms) == 0 } + return len(p.terms) == 0 } +// AsString returns the search query in string type func (p *Pattern) AsString() string { return string(p.text) } +// CacheKey is used to build string to be used as the key of result cache func (p *Pattern) CacheKey() string { - if p.mode == MODE_FUZZY { + if p.mode == ModeFuzzy { return p.AsString() } cacheableTerms := []string{} @@ -184,6 +188,7 @@ func (p *Pattern) CacheKey() string { return strings.Join(cacheableTerms, " ") } +// Match returns the list of matches Items in the given Chunk func (p *Pattern) Match(chunk *Chunk) []*Item { space := chunk @@ -213,7 +218,7 @@ Loop: } var matches []*Item - if p.mode == MODE_FUZZY { + if p.mode == ModeFuzzy { matches = p.fuzzyMatch(space) } else { matches = p.extendedMatch(space) diff --git a/src/pattern_test.go b/src/pattern_test.go index 2635b6c..c006c45 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -3,17 +3,17 @@ package fzf import "testing" func TestParseTermsExtended(t *testing.T) { - terms := parseTerms(MODE_EXTENDED, + terms := parseTerms(ModeExtended, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") if len(terms) != 8 || - terms[0].typ != TERM_FUZZY || terms[0].inv || - terms[1].typ != TERM_EXACT || terms[1].inv || - terms[2].typ != TERM_PREFIX || terms[2].inv || - terms[3].typ != TERM_SUFFIX || terms[3].inv || - terms[4].typ != TERM_FUZZY || !terms[4].inv || - terms[5].typ != TERM_EXACT || !terms[5].inv || - terms[6].typ != TERM_PREFIX || !terms[6].inv || - terms[7].typ != TERM_SUFFIX || !terms[7].inv { + terms[0].typ != termFuzzy || terms[0].inv || + terms[1].typ != termExact || terms[1].inv || + terms[2].typ != termPrefix || terms[2].inv || + terms[3].typ != termSuffix || terms[3].inv || + terms[4].typ != termFuzzy || !terms[4].inv || + terms[5].typ != termExact || !terms[5].inv || + terms[6].typ != termPrefix || !terms[6].inv || + terms[7].typ != termSuffix || !terms[7].inv { t.Errorf("%s", terms) } for idx, term := range terms { @@ -27,23 +27,23 @@ func TestParseTermsExtended(t *testing.T) { } func TestParseTermsExtendedExact(t *testing.T) { - terms := parseTerms(MODE_EXTENDED_EXACT, + terms := parseTerms(ModeExtendedExact, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") if len(terms) != 8 || - terms[0].typ != TERM_EXACT || terms[0].inv || len(terms[0].text) != 3 || - terms[1].typ != TERM_EXACT || terms[1].inv || len(terms[1].text) != 4 || - terms[2].typ != TERM_PREFIX || terms[2].inv || len(terms[2].text) != 3 || - terms[3].typ != TERM_SUFFIX || terms[3].inv || len(terms[3].text) != 3 || - terms[4].typ != TERM_EXACT || !terms[4].inv || len(terms[4].text) != 3 || - terms[5].typ != TERM_EXACT || !terms[5].inv || len(terms[5].text) != 4 || - terms[6].typ != TERM_PREFIX || !terms[6].inv || len(terms[6].text) != 3 || - terms[7].typ != TERM_SUFFIX || !terms[7].inv || len(terms[7].text) != 3 { + terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 || + terms[1].typ != termExact || terms[1].inv || len(terms[1].text) != 4 || + terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 || + terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 || + terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 || + terms[5].typ != termExact || !terms[5].inv || len(terms[5].text) != 4 || + terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 || + terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 { t.Errorf("%s", terms) } } func TestParseTermsEmpty(t *testing.T) { - terms := parseTerms(MODE_EXTENDED, "' $ ^ !' !^ !$") + terms := parseTerms(ModeExtended, "' $ ^ !' !^ !$") if len(terms) != 0 { t.Errorf("%s", terms) } @@ -52,7 +52,7 @@ func TestParseTermsEmpty(t *testing.T) { func TestExact(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(MODE_EXTENDED, CASE_SMART, + pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("'abc")) str := "aabbcc abc" sidx, eidx := ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text) @@ -64,17 +64,17 @@ func TestExact(t *testing.T) { func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() - pat1 := BuildPattern(MODE_FUZZY, CASE_SMART, []Range{}, nil, []rune("abc")) + pat1 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("abc")) clearPatternCache() - pat2 := BuildPattern(MODE_FUZZY, CASE_SMART, []Range{}, nil, []rune("Abc")) + pat2 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("Abc")) clearPatternCache() - pat3 := BuildPattern(MODE_FUZZY, CASE_IGNORE, []Range{}, nil, []rune("abc")) + pat3 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("abc")) clearPatternCache() - pat4 := BuildPattern(MODE_FUZZY, CASE_IGNORE, []Range{}, nil, []rune("Abc")) + pat4 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("Abc")) clearPatternCache() - pat5 := BuildPattern(MODE_FUZZY, CASE_RESPECT, []Range{}, nil, []rune("abc")) + pat5 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("abc")) clearPatternCache() - pat6 := BuildPattern(MODE_FUZZY, CASE_RESPECT, []Range{}, nil, []rune("Abc")) + pat6 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || @@ -90,7 +90,7 @@ func TestOrigTextAndTransformed(t *testing.T) { strptr := func(str string) *string { return &str } - pattern := BuildPattern(MODE_EXTENDED, CASE_SMART, []Range{}, nil, []rune("jg")) + pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("jg")) tokens := Tokenize(strptr("junegunn"), nil) trans := Transform(tokens, []Range{Range{1, 1}}) diff --git a/src/reader.go b/src/reader.go index 39fa70c..269a2fd 100644 --- a/src/reader.go +++ b/src/reader.go @@ -10,31 +10,33 @@ import ( "os/exec" ) -const DEFAULT_COMMAND = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` +const defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` +// Reader reads from command or standard input type Reader struct { pusher func(string) eventBox *EventBox } +// ReadSource reads data from the default command or from standard input func (r *Reader) ReadSource() { if int(C.isatty(C.int(os.Stdin.Fd()))) != 0 { cmd := os.Getenv("FZF_DEFAULT_COMMAND") if len(cmd) == 0 { - cmd = DEFAULT_COMMAND + cmd = defaultCommand } r.readFromCommand(cmd) } else { r.readFromStdin() } - r.eventBox.Set(EVT_READ_FIN, nil) + r.eventBox.Set(EvtReadFin, nil) } func (r *Reader) feed(src io.Reader) { if scanner := bufio.NewScanner(src); scanner != nil { for scanner.Scan() { r.pusher(scanner.Text()) - r.eventBox.Set(EVT_READ_NEW, nil) + r.eventBox.Set(EvtReadNew, nil) } } } diff --git a/src/reader_test.go b/src/reader_test.go index f51ccab..630f6fa 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -10,8 +10,8 @@ func TestReadFromCommand(t *testing.T) { eventBox: eb} // Check EventBox - if eb.Peak(EVT_READ_NEW) { - t.Error("EVT_READ_NEW should not be set yet") + if eb.Peak(EvtReadNew) { + t.Error("EvtReadNew should not be set yet") } // Normal command @@ -21,21 +21,21 @@ func TestReadFromCommand(t *testing.T) { } // Check EventBox again - if !eb.Peak(EVT_READ_NEW) { - t.Error("EVT_READ_NEW should be set yet") + if !eb.Peak(EvtReadNew) { + t.Error("EvtReadNew should be set yet") } // Wait should return immediately eb.Wait(func(events *Events) { - if _, found := (*events)[EVT_READ_NEW]; !found { + if _, found := (*events)[EvtReadNew]; !found { t.Errorf("%s", events) } events.Clear() }) // EventBox is cleared - if eb.Peak(EVT_READ_NEW) { - t.Error("EVT_READ_NEW should not be set yet") + if eb.Peak(EvtReadNew) { + t.Error("EvtReadNew should not be set yet") } // Failing command @@ -46,7 +46,7 @@ func TestReadFromCommand(t *testing.T) { } // Check EventBox again - if eb.Peak(EVT_READ_NEW) { - t.Error("Command failed. EVT_READ_NEW should be set") + if eb.Peak(EvtReadNew) { + t.Error("Command failed. EvtReadNew should be set") } } diff --git a/src/terminal.go b/src/terminal.go index 7d8bc5b..daf63c5 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -11,6 +11,7 @@ import ( "time" ) +// Terminal represents terminal input/output type Terminal struct { prompt string reverse bool @@ -34,23 +35,24 @@ type Terminal struct { suppress bool } -var _spinner []string = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} +var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} const ( - REQ_PROMPT EventType = iota - REQ_INFO - REQ_LIST - REQ_REFRESH - REQ_REDRAW - REQ_CLOSE - REQ_QUIT + reqPrompt EventType = iota + reqInfo + reqList + reqRefresh + reqRedraw + reqClose + reqQuit ) const ( - INITIAL_DELAY = 100 * time.Millisecond - SPINNER_DURATION = 200 * time.Millisecond + initialDelay = 100 * time.Millisecond + spinnerDuration = 200 * time.Millisecond ) +// NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { input := []rune(opts.Query) return &Terminal{ @@ -75,23 +77,26 @@ func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { }} } +// Input returns current query string func (t *Terminal) Input() []rune { t.mutex.Lock() defer t.mutex.Unlock() return copySlice(t.input) } +// UpdateCount updates the count information func (t *Terminal) UpdateCount(cnt int, final bool) { t.mutex.Lock() t.count = cnt t.reading = !final t.mutex.Unlock() - t.reqBox.Set(REQ_INFO, nil) + t.reqBox.Set(reqInfo, nil) if final { - t.reqBox.Set(REQ_REFRESH, nil) + t.reqBox.Set(reqRefresh, nil) } } +// UpdateProgress updates the search progress func (t *Terminal) UpdateProgress(progress float32) { t.mutex.Lock() newProgress := int(progress * 100) @@ -100,25 +105,25 @@ func (t *Terminal) UpdateProgress(progress float32) { t.mutex.Unlock() if changed { - t.reqBox.Set(REQ_INFO, nil) + t.reqBox.Set(reqInfo, nil) } } +// UpdateList updates Merger to display the list func (t *Terminal) UpdateList(merger *Merger) { t.mutex.Lock() t.progress = 100 t.merger = merger t.mutex.Unlock() - t.reqBox.Set(REQ_INFO, nil) - t.reqBox.Set(REQ_LIST, nil) + t.reqBox.Set(reqInfo, nil) + t.reqBox.Set(reqList, nil) } func (t *Terminal) listIndex(y int) int { if t.tac { return t.merger.Length() - y - 1 - } else { - return y } + return y } func (t *Terminal) output() { @@ -127,7 +132,7 @@ func (t *Terminal) output() { } if len(t.selected) == 0 { if t.merger.Length() > t.cy { - t.merger.Get(t.listIndex(t.cy)).Print() + fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString()) } } else { for ptr, orig := range t.selected { @@ -167,16 +172,16 @@ func (t *Terminal) placeCursor() { func (t *Terminal) printPrompt() { t.move(0, 0, true) - C.CPrint(C.COL_PROMPT, true, t.prompt) - C.CPrint(C.COL_NORMAL, true, string(t.input)) + C.CPrint(C.ColPrompt, true, t.prompt) + C.CPrint(C.ColNormal, true, string(t.input)) } func (t *Terminal) printInfo() { t.move(1, 0, true) if t.reading { - duration := int64(SPINNER_DURATION) + duration := int64(spinnerDuration) idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration - C.CPrint(C.COL_SPINNER, true, _spinner[idx]) + C.CPrint(C.ColSpinner, true, _spinner[idx]) } t.move(1, 2, false) @@ -187,7 +192,7 @@ func (t *Terminal) printInfo() { if t.progress > 0 && t.progress < 100 { output += fmt.Sprintf(" (%d%%)", t.progress) } - C.CPrint(C.COL_INFO, false, output) + C.CPrint(C.ColInfo, false, output) } func (t *Terminal) printList() { @@ -206,21 +211,21 @@ func (t *Terminal) printList() { func (t *Terminal) printItem(item *Item, current bool) { _, selected := t.selected[item.text] if current { - C.CPrint(C.COL_CURSOR, true, ">") + C.CPrint(C.ColCursor, true, ">") if selected { - C.CPrint(C.COL_CURRENT, true, ">") + C.CPrint(C.ColCurrent, true, ">") } else { - C.CPrint(C.COL_CURRENT, true, " ") + C.CPrint(C.ColCurrent, true, " ") } - t.printHighlighted(item, true, C.COL_CURRENT, C.COL_CURRENT_MATCH) + t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch) } else { - C.CPrint(C.COL_CURSOR, true, " ") + C.CPrint(C.ColCursor, true, " ") if selected { - C.CPrint(C.COL_SELECTED, true, ">") + C.CPrint(C.ColSelected, true, ">") } else { C.Print(" ") } - t.printHighlighted(item, false, 0, C.COL_MATCH) + t.printHighlighted(item, false, 0, C.ColMatch) } } @@ -232,25 +237,25 @@ func trimRight(runes []rune, width int) ([]rune, int) { sz := len(runes) currentWidth -= runewidth.RuneWidth(runes[sz-1]) runes = runes[:sz-1] - trimmed += 1 + trimmed++ } return runes, trimmed } func trimLeft(runes []rune, width int) ([]rune, int32) { currentWidth := displayWidth(runes) - var trimmed int32 = 0 + var trimmed int32 for currentWidth > width && len(runes) > 0 { currentWidth -= runewidth.RuneWidth(runes[0]) runes = runes[1:] - trimmed += 1 + trimmed++ } return runes, trimmed } func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { - var maxe int32 = 0 + var maxe int32 for _, offset := range item.offsets { if offset[1] > maxe { maxe = offset[1] @@ -293,7 +298,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { } sort.Sort(ByOrder(offsets)) - var index int32 = 0 + var index int32 for _, offset := range offsets { b := Max32(index, offset[0]) e := Max32(index, offset[1]) @@ -364,6 +369,7 @@ func (t *Terminal) rubout(pattern string) { t.input = append(t.input[:t.cx], after...) } +// Loop is called to start Terminal I/O func (t *Terminal) Loop() { { // Late initialization t.mutex.Lock() @@ -374,9 +380,9 @@ func (t *Terminal) Loop() { t.printInfo() t.mutex.Unlock() go func() { - timer := time.NewTimer(INITIAL_DELAY) + timer := time.NewTimer(initialDelay) <-timer.C - t.reqBox.Set(REQ_REFRESH, nil) + t.reqBox.Set(reqRefresh, nil) }() } @@ -387,22 +393,22 @@ func (t *Terminal) Loop() { t.mutex.Lock() for req := range *events { switch req { - case REQ_PROMPT: + case reqPrompt: t.printPrompt() - case REQ_INFO: + case reqInfo: t.printInfo() - case REQ_LIST: + case reqList: t.printList() - case REQ_REFRESH: + case reqRefresh: t.suppress = false - case REQ_REDRAW: + case reqRedraw: C.Clear() t.printAll() - case REQ_CLOSE: + case reqClose: C.Close() t.output() os.Exit(0) - case REQ_QUIT: + case reqQuit: C.Close() os.Exit(1) } @@ -420,11 +426,11 @@ func (t *Terminal) Loop() { t.mutex.Lock() previousInput := t.input - events := []EventType{REQ_PROMPT} + events := []EventType{reqPrompt} req := func(evts ...EventType) { for _, event := range evts { events = append(events, event) - if event == REQ_CLOSE || event == REQ_QUIT { + if event == reqClose || event == reqQuit { looping = false } } @@ -438,99 +444,99 @@ func (t *Terminal) Loop() { } else { delete(t.selected, item.text) } - req(REQ_INFO) + req(reqInfo) } } switch event.Type { - case C.INVALID: + case C.Invalid: t.mutex.Unlock() continue - case C.CTRL_A: + case C.CtrlA: t.cx = 0 - case C.CTRL_B: + case C.CtrlB: if t.cx > 0 { - t.cx -= 1 + t.cx-- } - case C.CTRL_C, C.CTRL_G, C.CTRL_Q, C.ESC: - req(REQ_QUIT) - case C.CTRL_D: + case C.CtrlC, C.CtrlG, C.CtrlQ, C.ESC: + req(reqQuit) + case C.CtrlD: if !t.delChar() && t.cx == 0 { - req(REQ_QUIT) + req(reqQuit) } - case C.CTRL_E: + case C.CtrlE: t.cx = len(t.input) - case C.CTRL_F: + case C.CtrlF: if t.cx < len(t.input) { - t.cx += 1 + t.cx++ } - case C.CTRL_H: + case C.CtrlH: if t.cx > 0 { t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) - t.cx -= 1 + t.cx-- } - case C.TAB: + case C.Tab: if t.multi && t.merger.Length() > 0 { toggle() t.vmove(-1) - req(REQ_LIST) + req(reqList) } - case C.BTAB: + case C.BTab: if t.multi && t.merger.Length() > 0 { toggle() t.vmove(1) - req(REQ_LIST) + req(reqList) } - case C.CTRL_J, C.CTRL_N: + case C.CtrlJ, C.CtrlN: t.vmove(-1) - req(REQ_LIST) - case C.CTRL_K, C.CTRL_P: + req(reqList) + case C.CtrlK, C.CtrlP: t.vmove(1) - req(REQ_LIST) - case C.CTRL_M: - req(REQ_CLOSE) - case C.CTRL_L: - req(REQ_REDRAW) - case C.CTRL_U: + req(reqList) + case C.CtrlM: + req(reqClose) + case C.CtrlL: + req(reqRedraw) + case C.CtrlU: if t.cx > 0 { t.yanked = copySlice(t.input[:t.cx]) t.input = t.input[t.cx:] t.cx = 0 } - case C.CTRL_W: + case C.CtrlW: if t.cx > 0 { t.rubout("\\s\\S") } - case C.ALT_BS: + case C.AltBS: if t.cx > 0 { t.rubout("[^[:alnum:]][[:alnum:]]") } - case C.CTRL_Y: + case C.CtrlY: t.input = append(append(t.input[:t.cx], t.yanked...), t.input[t.cx:]...) t.cx += len(t.yanked) - case C.DEL: + case C.Del: t.delChar() - case C.PGUP: + case C.PgUp: t.vmove(maxItems() - 1) - req(REQ_LIST) - case C.PGDN: + req(reqList) + case C.PgDn: t.vmove(-(maxItems() - 1)) - req(REQ_LIST) - case C.ALT_B: + req(reqList) + case C.AltB: t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 - case C.ALT_F: + case C.AltF: t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 - case C.ALT_D: + case C.AltD: ncx := t.cx + findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 if ncx > t.cx { t.yanked = copySlice(t.input[t.cx:ncx]) t.input = append(t.input[:t.cx], t.input[ncx:]...) } - case C.RUNE: + case C.Rune: prefix := copySlice(t.input[:t.cx]) t.input = append(append(prefix, event.Char), t.input[t.cx:]...) - t.cx += 1 - case C.MOUSE: + t.cx++ + case C.Mouse: me := event.MouseEvent mx, my := Min(len(t.input), Max(0, me.X-len(t.prompt))), me.Y if !t.reverse { @@ -543,13 +549,13 @@ func (t *Terminal) Loop() { toggle() } t.vmove(me.S) - req(REQ_LIST) + req(reqList) } } else if me.Double { // Double-click if my >= 2 { if t.vset(my-2) && t.listIndex(t.cy) < t.merger.Length() { - req(REQ_CLOSE) + req(reqClose) } } } else if me.Down { @@ -561,7 +567,7 @@ func (t *Terminal) Loop() { if t.vset(t.offset+my-2) && t.multi && me.Mod { toggle() } - req(REQ_LIST) + req(reqList) } } } @@ -569,7 +575,7 @@ func (t *Terminal) Loop() { t.mutex.Unlock() // Must be unlocked before touching reqBox if changed { - t.eventBox.Set(EVT_SEARCH_NEW, nil) + t.eventBox.Set(EvtSearchNew, nil) } for _, event := range events { t.reqBox.Set(event, nil) diff --git a/src/tokenizer.go b/src/tokenizer.go index d62f395..294329b 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -6,40 +6,42 @@ import ( "strings" ) -const RANGE_ELLIPSIS = 0 +const rangeEllipsis = 0 +// Range represents nth-expression type Range struct { begin int end int } +// Transformed holds the result of tokenization and transformation type Transformed struct { whole *string parts []Token } +// Token contains the tokenized part of the strings and its prefix length type Token struct { text *string prefixLength int } +// ParseRange parses nth-expression and returns the corresponding Range object func ParseRange(str *string) (Range, bool) { if (*str) == ".." { - return Range{RANGE_ELLIPSIS, RANGE_ELLIPSIS}, true + return Range{rangeEllipsis, rangeEllipsis}, true } else if strings.HasPrefix(*str, "..") { end, err := strconv.Atoi((*str)[2:]) if err != nil || end == 0 { return Range{}, false - } else { - return Range{RANGE_ELLIPSIS, end}, true } + return Range{rangeEllipsis, end}, true } else if strings.HasSuffix(*str, "..") { begin, err := strconv.Atoi((*str)[:len(*str)-2]) if err != nil || begin == 0 { return Range{}, false - } else { - return Range{begin, RANGE_ELLIPSIS}, true } + return Range{begin, rangeEllipsis}, true } else if strings.Contains(*str, "..") { ns := strings.Split(*str, "..") if len(ns) != 2 { @@ -75,9 +77,9 @@ func withPrefixLengths(tokens []string, begin int) []Token { } const ( - AWK_NIL = iota - AWK_BLACK - AWK_WHITE + awkNil = iota + awkBlack + awkWhite ) func awkTokenizer(input *string) ([]string, int) { @@ -85,28 +87,28 @@ func awkTokenizer(input *string) ([]string, int) { ret := []string{} str := []rune{} prefixLength := 0 - state := AWK_NIL + state := awkNil for _, r := range []rune(*input) { white := r == 9 || r == 32 switch state { - case AWK_NIL: + case awkNil: if white { prefixLength++ } else { - state = AWK_BLACK + state = awkBlack str = append(str, r) } - case AWK_BLACK: + case awkBlack: str = append(str, r) if white { - state = AWK_WHITE + state = awkWhite } - case AWK_WHITE: + case awkWhite: if white { str = append(str, r) } else { ret = append(ret, string(str)) - state = AWK_BLACK + state = awkBlack str = []rune{r} } } @@ -117,15 +119,15 @@ func awkTokenizer(input *string) ([]string, int) { return ret, prefixLength } +// Tokenize tokenizes the given string with the delimiter func Tokenize(str *string, delimiter *regexp.Regexp) []Token { if delimiter == nil { // AWK-style (\S+\s*) tokens, prefixLength := awkTokenizer(str) return withPrefixLengths(tokens, prefixLength) - } else { - tokens := delimiter.FindAllString(*str, -1) - return withPrefixLengths(tokens, 0) } + tokens := delimiter.FindAllString(*str, -1) + return withPrefixLengths(tokens, 0) } func joinTokens(tokens []Token) string { @@ -136,6 +138,7 @@ func joinTokens(tokens []Token) string { return ret } +// Transform is used to transform the input when --with-nth option is given func Transform(tokens []Token, withNth []Range) *Transformed { transTokens := make([]Token, len(withNth)) numTokens := len(tokens) @@ -145,7 +148,7 @@ func Transform(tokens []Token, withNth []Range) *Transformed { minIdx := 0 if r.begin == r.end { idx := r.begin - if idx == RANGE_ELLIPSIS { + if idx == rangeEllipsis { part += joinTokens(tokens) } else { if idx < 0 { @@ -158,12 +161,12 @@ func Transform(tokens []Token, withNth []Range) *Transformed { } } else { var begin, end int - if r.begin == RANGE_ELLIPSIS { // ..N + if r.begin == rangeEllipsis { // ..N begin, end = 1, r.end if end < 0 { end += numTokens + 1 } - } else if r.end == RANGE_ELLIPSIS { // N.. + } else if r.end == rangeEllipsis { // N.. begin, end = r.begin, numTokens if begin < 0 { begin += numTokens + 1 diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index 1ae0c7e..5195a1b 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -6,14 +6,14 @@ func TestParseRange(t *testing.T) { { i := ".." r, _ := ParseRange(&i) - if r.begin != RANGE_ELLIPSIS || r.end != RANGE_ELLIPSIS { + if r.begin != rangeEllipsis || r.end != rangeEllipsis { t.Errorf("%s", r) } } { i := "3.." r, _ := ParseRange(&i) - if r.begin != 3 || r.end != RANGE_ELLIPSIS { + if r.begin != 3 || r.end != rangeEllipsis { t.Errorf("%s", r) } } diff --git a/src/util.go b/src/util.go index de6f365..5461705 100644 --- a/src/util.go +++ b/src/util.go @@ -2,6 +2,7 @@ package fzf import "time" +// Max returns the largest integer func Max(first int, items ...int) int { max := first for _, item := range items { @@ -12,6 +13,7 @@ func Max(first int, items ...int) int { return max } +// Max32 returns the largest 32-bit integer func Max32(first int32, second int32) int32 { if first > second { return first @@ -19,6 +21,7 @@ func Max32(first int32, second int32) int32 { return second } +// Min returns the smallest integer func Min(first int, items ...int) int { min := first for _, item := range items { @@ -29,6 +32,7 @@ func Min(first int, items ...int) int { return min } +// DurWithin limits the given time.Duration with the upper and lower bounds func DurWithin( val time.Duration, min time.Duration, max time.Duration) time.Duration { if val < min { From cd847affb79ea6438c9721635724efc6f58e2215 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 12 Jan 2015 12:56:17 +0900 Subject: [PATCH 049/408] Reorganize source code --- src/Makefile | 2 +- src/{ => algo}/algo.go | 2 +- src/{ => algo}/algo_test.go | 2 +- src/chunklist.go | 10 ++++---- src/constants.go | 9 +++---- src/core.go | 10 ++++---- src/matcher.go | 18 +++++++------- src/options.go | 3 ++- src/pattern.go | 12 ++++++---- src/pattern_test.go | 8 +++++-- src/reader.go | 9 ++++--- src/reader_test.go | 10 +++++--- src/terminal.go | 39 +++++++++++++++++-------------- src/tokenizer.go | 4 +++- src/{ => util}/atomicbool.go | 2 +- src/{ => util}/atomicbool_test.go | 2 +- src/{ => util}/eventbox.go | 5 +++- src/{ => util}/eventbox_test.go | 12 +++++++++- src/{ => util}/util.go | 31 ++++++++++++++++-------- src/util/util_test.go | 22 +++++++++++++++++ src/util_test.go | 18 -------------- 21 files changed, 139 insertions(+), 91 deletions(-) rename src/{ => algo}/algo.go (99%) rename src/{ => algo}/algo_test.go (99%) rename src/{ => util}/atomicbool.go (98%) rename src/{ => util}/atomicbool_test.go (95%) rename src/{ => util}/eventbox.go (95%) rename src/{ => util}/eventbox_test.go (85%) rename src/{ => util}/util.go (60%) create mode 100644 src/util/util_test.go delete mode 100644 src/util_test.go diff --git a/src/Makefile b/src/Makefile index 7108f0d..43a7bc0 100644 --- a/src/Makefile +++ b/src/Makefile @@ -33,7 +33,7 @@ build: fzf/$(BINARY32) fzf/$(BINARY64) test: go get - go test -v + go test -v ./... install: $(BINDIR)/fzf diff --git a/src/algo.go b/src/algo/algo.go similarity index 99% rename from src/algo.go rename to src/algo/algo.go index 5f15ab3..bc4e538 100644 --- a/src/algo.go +++ b/src/algo/algo.go @@ -1,4 +1,4 @@ -package fzf +package algo import "strings" diff --git a/src/algo_test.go b/src/algo/algo_test.go similarity index 99% rename from src/algo_test.go rename to src/algo/algo_test.go index 5da01a6..363b6ee 100644 --- a/src/algo_test.go +++ b/src/algo/algo_test.go @@ -1,4 +1,4 @@ -package fzf +package algo import ( "strings" diff --git a/src/chunklist.go b/src/chunklist.go index 73983b1..571a59a 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -8,20 +8,20 @@ const ChunkSize int = 100 // Chunk is a list of Item pointers whose size has the upper limit of ChunkSize type Chunk []*Item // >>> []Item -// Transformer is a closure type that builds Item object from a pointer to a +// ItemBuilder is a closure type that builds Item object from a pointer to a // string and an integer -type Transformer func(*string, int) *Item +type ItemBuilder func(*string, int) *Item // ChunkList is a list of Chunks type ChunkList struct { chunks []*Chunk count int mutex sync.Mutex - trans Transformer + trans ItemBuilder } // NewChunkList returns a new ChunkList -func NewChunkList(trans Transformer) *ChunkList { +func NewChunkList(trans ItemBuilder) *ChunkList { return &ChunkList{ chunks: []*Chunk{}, count: 0, @@ -29,7 +29,7 @@ func NewChunkList(trans Transformer) *ChunkList { trans: trans} } -func (c *Chunk) push(trans Transformer, data *string, index int) { +func (c *Chunk) push(trans ItemBuilder, data *string, index int) { *c = append(*c, trans(data, index)) } diff --git a/src/constants.go b/src/constants.go index 80eb634..a871570 100644 --- a/src/constants.go +++ b/src/constants.go @@ -1,14 +1,15 @@ package fzf +import ( + "github.com/junegunn/fzf/src/util" +) + // Current version const Version = "0.9.0" -// EventType is the type for fzf events -type EventType int - // fzf events const ( - EvtReadNew EventType = iota + EvtReadNew util.EventType = iota EvtReadFin EvtSearchNew EvtSearchProgress diff --git a/src/core.go b/src/core.go index 65e641c..ee90413 100644 --- a/src/core.go +++ b/src/core.go @@ -30,6 +30,8 @@ import ( "os" "runtime" "time" + + "github.com/junegunn/fzf/src/util" ) const coordinatorDelayMax time.Duration = 100 * time.Millisecond @@ -59,7 +61,7 @@ func Run(options *Options) { } // Event channel - eventBox := NewEventBox() + eventBox := util.NewEventBox() // Chunk list var chunkList *ChunkList @@ -111,7 +113,7 @@ func Run(options *Options) { looping := true eventBox.Unwatch(EvtReadNew) for looping { - eventBox.Wait(func(events *Events) { + eventBox.Wait(func(events *util.Events) { for evt := range *events { switch evt { case EvtReadFin: @@ -154,7 +156,7 @@ func Run(options *Options) { for { delay := true ticks++ - eventBox.Wait(func(events *Events) { + eventBox.Wait(func(events *util.Events) { defer events.Clear() for evt, value := range *events { switch evt { @@ -185,7 +187,7 @@ func Run(options *Options) { } }) if delay && reading { - dur := DurWithin( + dur := util.DurWithin( time.Duration(ticks)*coordinatorDelayStep, 0, coordinatorDelayMax) time.Sleep(dur) diff --git a/src/matcher.go b/src/matcher.go index b8be287..1ea9541 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -6,6 +6,8 @@ import ( "sort" "sync" "time" + + "github.com/junegunn/fzf/src/util" ) // MatchRequest represents a search request @@ -18,14 +20,14 @@ type MatchRequest struct { type Matcher struct { patternBuilder func([]rune) *Pattern sort bool - eventBox *EventBox - reqBox *EventBox + eventBox *util.EventBox + reqBox *util.EventBox partitions int mergerCache map[string]*Merger } const ( - reqRetry EventType = iota + reqRetry util.EventType = iota reqReset ) @@ -35,12 +37,12 @@ const ( // NewMatcher returns a new Matcher func NewMatcher(patternBuilder func([]rune) *Pattern, - sort bool, eventBox *EventBox) *Matcher { + sort bool, eventBox *util.EventBox) *Matcher { return &Matcher{ patternBuilder: patternBuilder, sort: sort, eventBox: eventBox, - reqBox: NewEventBox(), + reqBox: util.NewEventBox(), partitions: runtime.NumCPU(), mergerCache: make(map[string]*Merger)} } @@ -52,7 +54,7 @@ func (m *Matcher) Loop() { for { var request MatchRequest - m.reqBox.Wait(func(events *Events) { + m.reqBox.Wait(func(events *util.Events) { for _, val := range *events { switch val := val.(type) { case MatchRequest: @@ -128,7 +130,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { } pattern := request.pattern empty := pattern.IsEmpty() - cancelled := NewAtomicBool(false) + cancelled := util.NewAtomicBool(false) slices := m.sliceChunks(request.chunks) numSlices := len(slices) @@ -202,7 +204,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { pattern := m.patternBuilder(patternRunes) - var event EventType + var event util.EventType if cancel { event = reqReset } else { diff --git a/src/options.go b/src/options.go index cf0608b..e1dba29 100644 --- a/src/options.go +++ b/src/options.go @@ -2,10 +2,11 @@ package fzf import ( "fmt" - "github.com/junegunn/go-shellwords" "os" "regexp" "strings" + + "github.com/junegunn/go-shellwords" ) const usage = `usage: fzf [options] diff --git a/src/pattern.go b/src/pattern.go index 9f32de6..17e3b6b 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -4,6 +4,8 @@ import ( "regexp" "sort" "strings" + + "github.com/junegunn/fzf/src/algo" ) const uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -112,10 +114,10 @@ func BuildPattern(mode Mode, caseMode Case, delimiter: delimiter, procFun: make(map[termType]func(bool, *string, []rune) (int, int))} - ptr.procFun[termFuzzy] = FuzzyMatch - ptr.procFun[termExact] = ExactMatchNaive - ptr.procFun[termPrefix] = PrefixMatch - ptr.procFun[termSuffix] = SuffixMatch + ptr.procFun[termFuzzy] = algo.FuzzyMatch + ptr.procFun[termExact] = algo.ExactMatchNaive + ptr.procFun[termPrefix] = algo.PrefixMatch + ptr.procFun[termSuffix] = algo.SuffixMatch _patternCache[asString] = ptr return ptr @@ -245,7 +247,7 @@ func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { matches := []*Item{} for _, item := range *chunk { input := p.prepareInput(item) - if sidx, eidx := p.iter(FuzzyMatch, input, p.text); sidx >= 0 { + if sidx, eidx := p.iter(algo.FuzzyMatch, input, p.text); sidx >= 0 { matches = append(matches, dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}})) } diff --git a/src/pattern_test.go b/src/pattern_test.go index c006c45..4d36eda 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -1,6 +1,10 @@ package fzf -import "testing" +import ( + "testing" + + "github.com/junegunn/fzf/src/algo" +) func TestParseTermsExtended(t *testing.T) { terms := parseTerms(ModeExtended, @@ -55,7 +59,7 @@ func TestExact(t *testing.T) { pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("'abc")) str := "aabbcc abc" - sidx, eidx := ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text) + sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text) if sidx != 7 || eidx != 10 { t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) } diff --git a/src/reader.go b/src/reader.go index 269a2fd..2c10b8a 100644 --- a/src/reader.go +++ b/src/reader.go @@ -1,13 +1,12 @@ package fzf -// #include -import "C" - import ( "bufio" "io" "os" "os/exec" + + "github.com/junegunn/fzf/src/util" ) const defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` @@ -15,12 +14,12 @@ const defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l // Reader reads from command or standard input type Reader struct { pusher func(string) - eventBox *EventBox + eventBox *util.EventBox } // ReadSource reads data from the default command or from standard input func (r *Reader) ReadSource() { - if int(C.isatty(C.int(os.Stdin.Fd()))) != 0 { + if util.IsTty() { cmd := os.Getenv("FZF_DEFAULT_COMMAND") if len(cmd) == 0 { cmd = defaultCommand diff --git a/src/reader_test.go b/src/reader_test.go index 630f6fa..5800b3f 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -1,10 +1,14 @@ package fzf -import "testing" +import ( + "testing" + + "github.com/junegunn/fzf/src/util" +) func TestReadFromCommand(t *testing.T) { strs := []string{} - eb := NewEventBox() + eb := util.NewEventBox() reader := Reader{ pusher: func(s string) { strs = append(strs, s) }, eventBox: eb} @@ -26,7 +30,7 @@ func TestReadFromCommand(t *testing.T) { } // Wait should return immediately - eb.Wait(func(events *Events) { + eb.Wait(func(events *util.Events) { if _, found := (*events)[EvtReadNew]; !found { t.Errorf("%s", events) } diff --git a/src/terminal.go b/src/terminal.go index daf63c5..4204a1d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -2,13 +2,16 @@ package fzf import ( "fmt" - C "github.com/junegunn/fzf/src/curses" - "github.com/junegunn/go-runewidth" "os" "regexp" "sort" "sync" "time" + + C "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/util" + + "github.com/junegunn/go-runewidth" ) // Terminal represents terminal input/output @@ -28,8 +31,8 @@ type Terminal struct { reading bool merger *Merger selected map[*string]*string - reqBox *EventBox - eventBox *EventBox + reqBox *util.EventBox + eventBox *util.EventBox mutex sync.Mutex initFunc func() suppress bool @@ -38,7 +41,7 @@ type Terminal struct { var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} const ( - reqPrompt EventType = iota + reqPrompt util.EventType = iota reqInfo reqList reqRefresh @@ -53,7 +56,7 @@ const ( ) // NewTerminal returns new Terminal object -func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { +func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) return &Terminal{ prompt: opts.Prompt, @@ -68,7 +71,7 @@ func NewTerminal(opts *Options, eventBox *EventBox) *Terminal { printQuery: opts.PrintQuery, merger: EmptyMerger, selected: make(map[*string]*string), - reqBox: NewEventBox(), + reqBox: util.NewEventBox(), eventBox: eventBox, mutex: sync.Mutex{}, suppress: true, @@ -288,7 +291,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { b, e := offset[0], offset[1] b += 2 - diff e += 2 - diff - b = Max32(b, 2) + b = util.Max32(b, 2) if b < e { offsets[idx] = Offset{b, e} } @@ -300,8 +303,8 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { sort.Sort(ByOrder(offsets)) var index int32 for _, offset := range offsets { - b := Max32(index, offset[0]) - e := Max32(index, offset[1]) + 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])) index = e @@ -388,7 +391,7 @@ func (t *Terminal) Loop() { go func() { for { - t.reqBox.Wait(func(events *Events) { + t.reqBox.Wait(func(events *util.Events) { defer events.Clear() t.mutex.Lock() for req := range *events { @@ -426,8 +429,8 @@ func (t *Terminal) Loop() { t.mutex.Lock() previousInput := t.input - events := []EventType{reqPrompt} - req := func(evts ...EventType) { + events := []util.EventType{reqPrompt} + req := func(evts ...util.EventType) { for _, event := range evts { events = append(events, event) if event == reqClose || event == reqQuit { @@ -538,7 +541,7 @@ func (t *Terminal) Loop() { t.cx++ case C.Mouse: me := event.MouseEvent - mx, my := Min(len(t.input), Max(0, me.X-len(t.prompt))), me.Y + mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y if !t.reverse { my = C.MaxY() - my - 1 } @@ -588,7 +591,7 @@ func (t *Terminal) constrain() { height := C.MaxY() - 2 diffpos := t.cy - t.offset - t.cy = Max(0, Min(t.cy, count-1)) + t.cy = util.Constrain(t.cy, 0, count-1) if t.cy > t.offset+(height-1) { // Ceil @@ -600,8 +603,8 @@ func (t *Terminal) constrain() { // Adjustment if count-t.offset < height { - t.offset = Max(0, count-height) - t.cy = Max(0, Min(t.offset+diffpos, count-1)) + t.offset = util.Max(0, count-height) + t.cy = util.Constrain(t.offset+diffpos, 0, count-1) } } @@ -614,7 +617,7 @@ func (t *Terminal) vmove(o int) { } func (t *Terminal) vset(o int) bool { - t.cy = Max(0, Min(o, t.merger.Length()-1)) + t.cy = util.Constrain(o, 0, t.merger.Length()-1) return t.cy == o } diff --git a/src/tokenizer.go b/src/tokenizer.go index 294329b..26aebd9 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -4,6 +4,8 @@ import ( "regexp" "strconv" "strings" + + "github.com/junegunn/fzf/src/util" ) const rangeEllipsis = 0 @@ -180,7 +182,7 @@ func Transform(tokens []Token, withNth []Range) *Transformed { end += numTokens + 1 } } - minIdx = Max(0, begin-1) + minIdx = util.Max(0, begin-1) for idx := begin; idx <= end; idx++ { if idx >= 1 && idx <= numTokens { part += *tokens[idx-1].text diff --git a/src/atomicbool.go b/src/util/atomicbool.go similarity index 98% rename from src/atomicbool.go rename to src/util/atomicbool.go index b264724..9e1bdc8 100644 --- a/src/atomicbool.go +++ b/src/util/atomicbool.go @@ -1,4 +1,4 @@ -package fzf +package util import "sync" diff --git a/src/atomicbool_test.go b/src/util/atomicbool_test.go similarity index 95% rename from src/atomicbool_test.go rename to src/util/atomicbool_test.go index 0af4570..1feff79 100644 --- a/src/atomicbool_test.go +++ b/src/util/atomicbool_test.go @@ -1,4 +1,4 @@ -package fzf +package util import "testing" diff --git a/src/eventbox.go b/src/util/eventbox.go similarity index 95% rename from src/eventbox.go rename to src/util/eventbox.go index 0c8f922..568ad9f 100644 --- a/src/eventbox.go +++ b/src/util/eventbox.go @@ -1,7 +1,10 @@ -package fzf +package util import "sync" +// EventType is the type for fzf events +type EventType int + // Events is a type that associates EventType to any data type Events map[EventType]interface{} diff --git a/src/eventbox_test.go b/src/util/eventbox_test.go similarity index 85% rename from src/eventbox_test.go rename to src/util/eventbox_test.go index 1cd7f22..5a9dc30 100644 --- a/src/eventbox_test.go +++ b/src/util/eventbox_test.go @@ -1,7 +1,17 @@ -package fzf +package util import "testing" +// fzf events +const ( + EvtReadNew EventType = iota + EvtReadFin + EvtSearchNew + EvtSearchProgress + EvtSearchFin + EvtClose +) + func TestEventBox(t *testing.T) { eb := NewEventBox() diff --git a/src/util.go b/src/util/util.go similarity index 60% rename from src/util.go rename to src/util/util.go index 5461705..14833c0 100644 --- a/src/util.go +++ b/src/util/util.go @@ -1,6 +1,12 @@ -package fzf +package util -import "time" +// #include +import "C" + +import ( + "os" + "time" +) // Max returns the largest integer func Max(first int, items ...int) int { @@ -21,15 +27,15 @@ func Max32(first int32, second int32) int32 { return second } -// Min returns the smallest integer -func Min(first int, items ...int) int { - min := first - for _, item := range items { - if item < min { - min = item - } +// Constrain limits the given integer with the upper and lower bounds +func Constrain(val int, min int, max int) int { + if val < min { + return min } - return min + if val > max { + return max + } + return val } // DurWithin limits the given time.Duration with the upper and lower bounds @@ -43,3 +49,8 @@ func DurWithin( } return val } + +// IsTty returns true is stdin is a terminal +func IsTty() bool { + return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 +} diff --git a/src/util/util_test.go b/src/util/util_test.go new file mode 100644 index 0000000..06cfd4f --- /dev/null +++ b/src/util/util_test.go @@ -0,0 +1,22 @@ +package util + +import "testing" + +func TestMax(t *testing.T) { + if Max(-2, 5, 1, 4, 3) != 5 { + t.Error("Invalid result") + } +} + +func TestContrain(t *testing.T) { + if Constrain(-3, -1, 3) != -1 { + t.Error("Expected", -1) + } + if Constrain(2, -1, 3) != 2 { + t.Error("Expected", 2) + } + + if Constrain(5, -1, 3) != 3 { + t.Error("Expected", 3) + } +} diff --git a/src/util_test.go b/src/util_test.go deleted file mode 100644 index 814b42c..0000000 --- a/src/util_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package fzf - -import "testing" - -func TestMax(t *testing.T) { - if Max(-2, 5, 1, 4, 3) != 5 { - t.Error("Invalid result") - } -} - -func TestMin(t *testing.T) { - if Min(2, -3) != -3 { - t.Error("Invalid result") - } - if Min(-2, 5, 1, 4, 3) != -2 { - t.Error("Invalid result") - } -} From 2c86e728b56a54304f4a4404f14a678b5279adc9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 13 Jan 2015 02:05:37 +0900 Subject: [PATCH 050/408] Update src/README.md --- src/README.md | 80 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/src/README.md b/src/README.md index fb17e68..c071865 100644 --- a/src/README.md +++ b/src/README.md @@ -2,30 +2,45 @@ fzf in Go ========= This directory contains the source code for the new fzf implementation in -[Go][go]. The new version has the following benefits over the previous Ruby -version. +[Go][go]. -Motivation ----------- +Upgrade from Ruby version +------------------------- + +The install script has been updated to download the right binary for your +system. If you already have installed fzf, simply git-pull the repository and +rerun the install script. + +```sh +cd ~/.fzf +git pull +./install +``` + +Motivations +----------- ### No Ruby dependency There have always been complaints about fzf being a Ruby script. To make -matters worse, Ruby 2.1 dropped ncurses support from its standard libary. -Because of the change, users running Ruby 2.1 or above were forced to build C +matters worse, Ruby 2.1 removed ncurses binding from its standard libary. +Because of the change, users running Ruby 2.1 or above are forced to build C extensions of curses gem to meet the requirement of fzf. The new Go version will be distributed as an executable binary so it will be much more accessible -and easier to setup. +and should be easier to setup. ### Performance -With the presence of [GIL][gil], Ruby cannot utilize multiple CPU cores. Even -though the Ruby version of fzf was pretty responsive even for 100k+ lines, -which is well above the size of the usual input, it was obvious that we could -do better. Now with the Go version, GIL is gone, and the search performance -scales proportional to the number of cores. On my Macbook Pro (Mid 2012), it -was shown to be an order of magnitude faster on certain cases. It also starts -much faster than before though the difference shouldn't be really noticeable. +Many people have been surprised to see how fast fzf is even when it was +written in Ruby. It stays quite responsive even for 100k+ lines, which is +well above the size of the usual input. + +The new Go version, of course, is significantly faster than that. It has all +the performance optimization techniques used in Ruby implementation and more. +It also doesn't suffer from [GIL][gil], so the search performance scales +proportional to the number of CPU cores. On my MacBook Pro (Mid 2012), the new +version was shown to be an order of magnitude faster on certain cases. It also +starts much faster though the difference may not be noticeable. Differences with Ruby version ----------------------------- @@ -40,19 +55,19 @@ even for 1M+ items) so I decided that it's no longer required. System requirements ------------------- -Currently prebuilt binaries are provided only for OS X and Linux. The install +Currently, prebuilt binaries are provided only for OS X and Linux. The install script will fall back to the legacy Ruby version on the other systems, but if you have Go 1.4 installed, you can try building it yourself. However, as pointed out in [golang.org/doc/install][req], the Go version may -not run on CentOS/RHEL 5.x and thus the install script will choose the Ruby -version instead. +not run on CentOS/RHEL 5.x, and if that's the case, the install script will +choose the Ruby version instead. The Go version depends on [ncurses][ncurses] and some Unix system calls, so it -shouldn't run natively on Windows at the moment. But it should be not -impossible to support Windows by falling back to a cross-platform alternative -such as [termbox][termbox] only on Windows. If you're interested in making fzf -work on Windows, please let me know. +shouldn't run natively on Windows at the moment. But it won't be impossible to +support Windows by falling back to a cross-platform alternative such as +[termbox][termbox] only on Windows. If you're interested in making fzf work on +Windows, please let me know. Build ----- @@ -68,6 +83,17 @@ make install make linux ``` +Contribution +------------ + +For the time being, I will not add or accept any new features until we can be +sure that the implementation is stable and we have a sufficient number of test +cases. However, fixes for obvious bugs and new test cases are welcome. + +I also care much about the performance of the implementation, so please make +sure that your change does not result in performance regression. And please be +noted that we don't have a quantitative measure of the performance yet. + Third-party libraries used -------------------------- @@ -77,18 +103,6 @@ Third-party libraries used - [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - Licensed under [MIT](http://mattn.mit-license.org/2014) -Contribution ------------- - -For the time being, I will not add or accept any new features until we can be -sure that the implementation is stable and we have a sufficient number of test -cases. However, fixes for obvious bugs and new test cases are welcome. - -I also care much about the performance of the implementation (that's the -reason I rewrote the whole thing in Go, right?), so please make sure that your -change does not result in performance regression. Please be minded that we -still don't have a quantitative measure of the performance. - License ------- From 5c491d573a147573c68aa7e56a6032dbe4b84635 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 13 Jan 2015 02:39:00 +0900 Subject: [PATCH 051/408] Fix fzf.{bash,zsh} when Go version is not supported --- install | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/install b/install index f8307cf..4098c87 100755 --- a/install +++ b/install @@ -152,20 +152,29 @@ for shell in bash zsh; do fzf_completion="# $fzf_completion" fi - cat > $src << EOF + if [ -n "$binary_error" ]; then + cat > $src << EOF # Setup fzf function # ------------------ unalias fzf 2> /dev/null +fzf() { + $fzf_cmd "\$@" +} +export -f fzf > /dev/null + +# Auto-completion +# --------------- +$fzf_completion + +EOF + else + cat > $src << EOF +# Setup fzf +# --------- +unalias fzf 2> /dev/null unset fzf 2> /dev/null -if [ -x "$fzf_base/bin/fzf" ]; then - if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then - export PATH="$fzf_base/bin:\$PATH" - fi -else - fzf() { - $fzf_cmd "\$@" - } - export -f fzf > /dev/null +if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then + export PATH="$fzf_base/bin:\$PATH" fi # Auto-completion @@ -173,6 +182,7 @@ fi $fzf_completion EOF + fi if [ $key_bindings -eq 0 ]; then if [ $shell = bash ]; then From b8c4b35415a096d245272556be5231f071404365 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 13 Jan 2015 12:29:12 +0900 Subject: [PATCH 052/408] `make archive` for homebrew release --- install | 5 +++-- src/Makefile | 19 ++++++++++++++++--- src/README.md | 3 +++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/install b/install index 4098c87..c61cf88 100755 --- a/install +++ b/install @@ -21,8 +21,9 @@ check_binary() { symlink() { echo " - Creating symlink: bin/$1 -> bin/fzf" - rm -f "$fzf_base"/bin/fzf - ln -sf "$fzf_base"/bin/$1 "$fzf_base"/bin/fzf + (cd "$fzf_base"/bin && + rm -f fzf + ln -sf $1 fzf) } download() { diff --git a/src/Makefile b/src/Makefile index 43a7bc0..2758164 100644 --- a/src/Makefile +++ b/src/Makefile @@ -18,11 +18,24 @@ BINDIR := ../bin BINARY32 := fzf-$(GOOS)_386 BINARY64 := fzf-$(GOOS)_amd64 -RELEASE32 = fzf-$(shell fzf/$(BINARY64) --version)-$(GOOS)_386 -RELEASE64 = fzf-$(shell fzf/$(BINARY64) --version)-$(GOOS)_amd64 +VERSION = $(shell fzf/$(BINARY64) --version) +RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 +RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 +ARCHIVE64 = fzf-$(VERSION)-$(GOOS)_amd64-archive all: test release +archive: ../$(ARCHIVE64).tar.gz + +../$(ARCHIVE64).tar.gz: release + cd ../ && yes | ./install && \ + git archive HEAD --output=$(ARCHIVE64).tar --prefix fzf-$(VERSION)/ && \ + rm -rf fzf-$(VERSION)/bin && mkdir -p fzf-$(VERSION)/bin && \ + cp -a bin/* fzf-$(VERSION)/bin/ && \ + tar --append --file $(ARCHIVE64).tar fzf-$(VERSION)/bin/* && \ + gzip -f $(ARCHIVE64).tar && rm -rf fzf-$(VERSION) && \ + openssl sha1 $(notdir $@) + release: build cd fzf && \ cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \ @@ -70,4 +83,4 @@ $(DISTRO): docker docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' -.PHONY: all build release test install uninstall clean docker linux $(DISTRO) +.PHONY: all archive build release test install uninstall clean docker linux $(DISTRO) diff --git a/src/README.md b/src/README.md index c071865..915e96a 100644 --- a/src/README.md +++ b/src/README.md @@ -81,6 +81,9 @@ make install # Build executables and tarballs for Linux using Docker make linux + +# Build tarball for Homebrew release +make archive ``` Contribution From a3068a33d5403bc0630606f3649c9086bd1e9269 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 13 Jan 2015 23:46:38 +0900 Subject: [PATCH 053/408] Update install/build script from Homebrew --- LICENSE | 21 +++++++++++++++++++++ README.md | 2 +- install | 15 +++++++++++---- src/Makefile | 27 +++++++++++++++------------ src/README.md | 2 +- 5 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fe4c31a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 998db0c..4b7c6c5 100644 --- a/README.md +++ b/README.md @@ -536,7 +536,7 @@ find . | iconv -f utf-8-mac -t utf8//ignore | fzf License ------- -MIT +[MIT](LICENSE) Author ------ diff --git a/install b/install index c61cf88..a481d57 100755 --- a/install +++ b/install @@ -13,9 +13,14 @@ ask() { check_binary() { echo -n " - Checking fzf executable ... " - if ! "$fzf_base"/bin/fzf --version; then + local output=$("$fzf_base"/bin/fzf --version 2>&1) + if [ "$version" = "$output" ]; then + echo "$output" + else + echo "$output != $version" rm -f "$fzf_base"/bin/fzf - binary_error="Error occurred" + binary_error="Invalid binary" + return 1 fi } @@ -27,9 +32,11 @@ symlink() { } download() { - echo "Downloading bin/$1 ..." - if [ -x "$fzf_base"/bin/$1 ]; then + echo "Downloading bin/fzf ..." + if [ -x "$fzf_base"/bin/fzf ]; then echo " - Already exists" + check_binary && return + elif [ -x "$fzf_base"/bin/$1 ]; then symlink $1 check_binary && return fi diff --git a/src/Makefile b/src/Makefile index 2758164..68067ef 100644 --- a/src/Makefile +++ b/src/Makefile @@ -21,26 +21,29 @@ BINARY64 := fzf-$(GOOS)_amd64 VERSION = $(shell fzf/$(BINARY64) --version) RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 -ARCHIVE64 = fzf-$(VERSION)-$(GOOS)_amd64-archive +BREW = fzf-$(VERSION)-homebrew.tgz all: test release -archive: ../$(ARCHIVE64).tar.gz +brew: ../$(BREW) -../$(ARCHIVE64).tar.gz: release - cd ../ && yes | ./install && \ - git archive HEAD --output=$(ARCHIVE64).tar --prefix fzf-$(VERSION)/ && \ - rm -rf fzf-$(VERSION)/bin && mkdir -p fzf-$(VERSION)/bin && \ - cp -a bin/* fzf-$(VERSION)/bin/ && \ - tar --append --file $(ARCHIVE64).tar fzf-$(VERSION)/bin/* && \ - gzip -f $(ARCHIVE64).tar && rm -rf fzf-$(VERSION) && \ +../$(BREW): release +ifneq ($(UNAME_S),Darwin) +$(error brew package must be built on OS X) +endif + mkdir -p ../bin && \ + cp fzf/$(RELEASE64) fzf/$(RELEASE32) ../bin && \ + cd .. && ln -sf . fzf-$(VERSION) && \ + tar -cvzf $(BREW) \ + fzf-$(VERSION)/{{,un}install,fzf-completion.{ba,z}sh,LICENSE} \ + fzf-$(VERSION)/{plugin/fzf.vim,bin/{$(RELEASE64),$(RELEASE32)}} && \ + rm fzf-$(VERSION) && \ openssl sha1 $(notdir $@) release: build cd fzf && \ cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \ - cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ - rm $(RELEASE32) $(RELEASE64) + cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) build: fzf/$(BINARY32) fzf/$(BINARY64) @@ -83,4 +86,4 @@ $(DISTRO): docker docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' -.PHONY: all archive build release test install uninstall clean docker linux $(DISTRO) +.PHONY: all brew build release test install uninstall clean docker linux $(DISTRO) diff --git a/src/README.md b/src/README.md index 915e96a..a15ab58 100644 --- a/src/README.md +++ b/src/README.md @@ -83,7 +83,7 @@ make install make linux # Build tarball for Homebrew release -make archive +make brew ``` Contribution From 789f26b1a53b84cb7bf136dcd7b40791a93b45e3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 14 Jan 2015 02:16:03 +0900 Subject: [PATCH 054/408] Add GIF to src/README --- src/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/README.md b/src/README.md index a15ab58..2368b50 100644 --- a/src/README.md +++ b/src/README.md @@ -1,6 +1,8 @@ fzf in Go ========= +fzf in go + This directory contains the source code for the new fzf implementation in [Go][go]. From 0b5fa56444350c575ed6aeb0d1d0ec35f61c00c2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 14 Jan 2015 02:26:47 +0900 Subject: [PATCH 055/408] Remove brew target --- src/Makefile | 21 +++------------------ src/README.md | 3 --- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/Makefile b/src/Makefile index 68067ef..4610e58 100644 --- a/src/Makefile +++ b/src/Makefile @@ -21,29 +21,14 @@ BINARY64 := fzf-$(GOOS)_amd64 VERSION = $(shell fzf/$(BINARY64) --version) RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 -BREW = fzf-$(VERSION)-homebrew.tgz all: test release -brew: ../$(BREW) - -../$(BREW): release -ifneq ($(UNAME_S),Darwin) -$(error brew package must be built on OS X) -endif - mkdir -p ../bin && \ - cp fzf/$(RELEASE64) fzf/$(RELEASE32) ../bin && \ - cd .. && ln -sf . fzf-$(VERSION) && \ - tar -cvzf $(BREW) \ - fzf-$(VERSION)/{{,un}install,fzf-completion.{ba,z}sh,LICENSE} \ - fzf-$(VERSION)/{plugin/fzf.vim,bin/{$(RELEASE64),$(RELEASE32)}} && \ - rm fzf-$(VERSION) && \ - openssl sha1 $(notdir $@) - release: build cd fzf && \ cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \ - cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) + cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ + rm $(RELEASE32) $(RELEASE64) build: fzf/$(BINARY32) fzf/$(BINARY64) @@ -86,4 +71,4 @@ $(DISTRO): docker docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' -.PHONY: all brew build release test install uninstall clean docker linux $(DISTRO) +.PHONY: all build release test install uninstall clean docker linux $(DISTRO) diff --git a/src/README.md b/src/README.md index 2368b50..511b744 100644 --- a/src/README.md +++ b/src/README.md @@ -83,9 +83,6 @@ make install # Build executables and tarballs for Linux using Docker make linux - -# Build tarball for Homebrew release -make brew ``` Contribution From 62acb9adc47e714a427f12cb2e2c6ebca0bd7f38 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 15 Jan 2015 06:06:22 +0900 Subject: [PATCH 056/408] Fix error with empty list and release 0.9.1-dev --- install | 4 ++-- src/constants.go | 2 +- src/terminal.go | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/install b/install index a481d57..e03c357 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.0 +version=0.9.1-dev cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) @@ -33,7 +33,7 @@ symlink() { download() { echo "Downloading bin/fzf ..." - if [ -x "$fzf_base"/bin/fzf ]; then + if [[ ! $1 =~ dev && -x "$fzf_base"/bin/fzf ]]; then echo " - Already exists" check_binary && return elif [ -x "$fzf_base"/bin/$1 ]; then diff --git a/src/constants.go b/src/constants.go index a871570..c3d8575 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.0" +const Version = "0.9.1-dev" // fzf events const ( diff --git a/src/terminal.go b/src/terminal.go index 4204a1d..44796ce 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -134,7 +134,8 @@ func (t *Terminal) output() { fmt.Println(string(t.input)) } if len(t.selected) == 0 { - if t.merger.Length() > t.cy { + cnt := t.merger.Length() + if cnt > 0 && cnt > t.cy { fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString()) } } else { From ae3180f91994cd6129dbb80144cda4b1abeb6ec0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 04:55:29 +0900 Subject: [PATCH 057/408] Fix CTRL-Y key binding With tmux-based test cases --- .travis.yml | 19 ++- Rakefile | 7 +- src/Makefile | 6 +- src/terminal.go | 3 +- test/test_go.rb | 184 +++++++++++++++++++++++++++++ test/{test_fzf.rb => test_ruby.rb} | 2 +- 6 files changed, 209 insertions(+), 12 deletions(-) create mode 100644 test/test_go.rb rename test/{test_fzf.rb => test_ruby.rb} (99%) diff --git a/.travis.yml b/.travis.yml index 7633c4f..3896190 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,19 @@ language: ruby -sudo: false rvm: - - "1.8.7" - - "1.9.3" - - "2.0.0" - "2.1.1" -install: gem install curses minitest +before_script: | + sudo apt-get install -y tmux libncurses-dev lib32ncurses5-dev + export GOROOT=~/go1.4 + export GOPATH=~/go + + mkdir -p ~/go/src/github.com/junegunn + ln -s $(pwd) ~/go/src/github.com/junegunn/fzf + + curl https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | tar -xz + mv go $GOROOT + cd $GOROOT/src && GOARCH=386 ./make.bash + + cd ~/go/src/github.com/junegunn/fzf/src && make install && cd .. && + yes | ./install && tmux diff --git a/Rakefile b/Rakefile index 933a039..5bc903d 100644 --- a/Rakefile +++ b/Rakefile @@ -2,8 +2,11 @@ require "bundler/gem_tasks" require 'rake/testtask' Rake::TestTask.new(:test) do |test| - test.pattern = 'test/**/test_*.rb' - test.verbose = true + test.pattern = 'test/test_go.rb' +end + +Rake::TestTask.new(:testall) do |test| + test.pattern = 'test/test_*.rb' end task :default => :test diff --git a/src/Makefile b/src/Makefile index 4610e58..eda0404 100644 --- a/src/Makefile +++ b/src/Makefile @@ -22,7 +22,7 @@ VERSION = $(shell fzf/$(BINARY64) --version) RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 -all: test release +all: release release: build cd fzf && \ @@ -30,13 +30,13 @@ release: build cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ rm $(RELEASE32) $(RELEASE64) -build: fzf/$(BINARY32) fzf/$(BINARY64) +build: test fzf/$(BINARY32) fzf/$(BINARY64) test: go get go test -v ./... -install: $(BINDIR)/fzf +install: build $(BINDIR)/fzf uninstall: rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64) diff --git a/src/terminal.go b/src/terminal.go index 44796ce..abd7069 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -515,7 +515,8 @@ func (t *Terminal) Loop() { t.rubout("[^[:alnum:]][[:alnum:]]") } case C.CtrlY: - t.input = append(append(t.input[:t.cx], t.yanked...), t.input[t.cx:]...) + suffix := copySlice(t.input[t.cx:]) + t.input = append(append(t.input[:t.cx], t.yanked...), suffix...) t.cx += len(t.yanked) case C.Del: t.delChar() diff --git a/test/test_go.rb b/test/test_go.rb new file mode 100644 index 0000000..de8fac7 --- /dev/null +++ b/test/test_go.rb @@ -0,0 +1,184 @@ +#!/usr/bin/env ruby +# encoding: utf-8 + +require 'minitest/autorun' + +class Tmux + TEMPNAME = '/tmp/fzf-test.txt' + + attr_reader :win + + def initialize shell = 'bash' + @win = go("new-window -P -F '#I' 'bash --rcfile ~/.fzf.#{shell}'").first + end + + def self.current + `tmux display-message -p '#I'`.split($/).first + end + + def self.select id + system "tmux select-window -t #{id}" + end + + def closed? + !go("list-window -F '#I'").include?(win) + end + + def close timeout = 1 + send_keys 'C-c', 'C-u', 'C-d' + wait(timeout) { closed? } + end + + def kill + go("kill-window -t #{win} 2> /dev/null") + end + + def send_keys *args + args = args.map { |a| %{"#{a}"} }.join ' ' + go("send-keys -t #{win} #{args}") + end + + def capture + go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}") + raise "Window not found" if $?.exitstatus != 0 + File.read(TEMPNAME).split($/) + end + + def until timeout = 1 + wait(timeout) { yield capture } + end + +private + def wait timeout = 1 + waited = 0 + until yield + waited += 0.1 + sleep 0.1 + raise "timeout" if waited > timeout + end + end + + def go *args + %x[tmux #{args.join ' '}].split($/) + end +end + +class TestGoFZF < MiniTest::Unit::TestCase + attr_reader :tmux + + def tempname + '/tmp/output' + end + + def setup + ENV.delete 'FZF_DEFAULT_OPTS' + ENV.delete 'FZF_DEFAULT_COMMAND' + @prev = Tmux.current + @tmux = Tmux.new + File.unlink tempname rescue nil + end + + def teardown + @tmux.kill + Tmux.select @prev + end + + def test_vanilla + tmux.send_keys "seq 1 100000 | fzf > #{tempname}", :Enter + tmux.until { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } + lines = tmux.capture + assert_equal ' 2', lines[-4] + assert_equal '> 1', lines[-3] + assert_equal ' 100000/100000', lines[-2] + assert_equal '>', lines[-1] + + # Testing basic key bindings + tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab' + tmux.until { |lines| lines.last == '> 391' } + lines = tmux.capture + assert_equal '> 1391', lines[-4] + assert_equal ' 391', lines[-3] + assert_equal ' 856/100000', lines[-2] + assert_equal '> 391', lines[-1] + + tmux.send_keys :Enter + tmux.close + assert_equal '1391', File.read(tempname).chomp + end + + def test_fzf_default_command + 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', File.read(tempname).chomp + end + + def test_fzf_prompt + tmux.send_keys "fzf -q 'foo bar foo-bar'", :Enter + tmux.until { |lines| lines.last =~ /foo-bar/ } + + # CTRL-A + tmux.send_keys "C-A", "(" + tmux.until { |lines| lines.last == '> (foo bar foo-bar' } + + # META-F + tmux.send_keys :Escape, :f, ")" + tmux.until { |lines| lines.last == '> (foo) bar foo-bar' } + + # CTRL-B + tmux.send_keys "C-B", "var" + tmux.until { |lines| lines.last == '> (foovar) bar foo-bar' } + + # Left, CTRL-D + tmux.send_keys :Left, :Left, "C-D" + tmux.until { |lines| lines.last == '> (foovr) bar foo-bar' } + + # META-BS + tmux.send_keys :Escape, :BSpace + tmux.until { |lines| lines.last == '> (r) bar foo-bar' } + + # CTRL-Y + tmux.send_keys "C-Y", "C-Y" + tmux.until { |lines| lines.last == '> (foovfoovr) bar foo-bar' } + + # META-B + tmux.send_keys :Escape, :b, :Space, :Space + tmux.until { |lines| lines.last == '> ( foovfoovr) bar foo-bar' } + + # CTRL-F / Right + tmux.send_keys 'C-F', :Right, '/' + tmux.until { |lines| lines.last == '> ( fo/ovfoovr) bar foo-bar' } + + # CTRL-H / BS + tmux.send_keys 'C-H', :BSpace + tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-bar' } + + # CTRL-E + tmux.send_keys "C-E", 'baz' + tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' } + + # CTRL-U + tmux.send_keys "C-U" + tmux.until { |lines| lines.last == '>' } + + # CTRL-Y + tmux.send_keys "C-Y" + tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' } + + # CTRL-W + tmux.send_keys "C-W", "bar-foo" + tmux.until { |lines| lines.last == '> ( fovfoovr) bar bar-foo' } + + # META-D + tmux.send_keys :Escape, :b, :Escape, :b, :Escape, :d, "C-A", "C-Y" + tmux.until { |lines| lines.last == '> bar( fovfoovr) bar -foo' } + + # CTRL-M + tmux.send_keys "C-M" + tmux.until { |lines| lines.last !~ /^>/ } + tmux.close + end +end + diff --git a/test/test_fzf.rb b/test/test_ruby.rb similarity index 99% rename from test/test_fzf.rb rename to test/test_ruby.rb index e11f2c5..674ed3b 100644 --- a/test/test_fzf.rb +++ b/test/test_ruby.rb @@ -54,7 +54,7 @@ class MockTTY end end -class TestFZF < MiniTest::Unit::TestCase +class TestRubyFZF < MiniTest::Unit::TestCase def setup ENV.delete 'FZF_DEFAULT_SORT' ENV.delete 'FZF_DEFAULT_OPTS' From c0b432f7b4086c415d798d23970a3f5a4471af32 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 06:12:57 +0900 Subject: [PATCH 058/408] Fix Travis-CI build --- .travis.yml | 24 +++++++++++++++--------- test/test_go.rb | 21 +++++++++++++++------ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3896190..9943ca6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,25 @@ language: ruby -rvm: - - "2.1.1" -before_script: | - sudo apt-get install -y tmux libncurses-dev lib32ncurses5-dev +install: +- sudo apt-get install -y libncurses-dev lib32ncurses5-dev +- sudo add-apt-repository -y ppa:pi-rho/dev +- sudo apt-get update +- sudo apt-get install -y tmux=1.9a-1~ppa1~p +script: | export GOROOT=~/go1.4 export GOPATH=~/go + export FZF_BASE=~/go/src/github.com/junegunn/fzf mkdir -p ~/go/src/github.com/junegunn - ln -s $(pwd) ~/go/src/github.com/junegunn/fzf + ln -s $(pwd) $FZF_BASE - curl https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | tar -xz + curl https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | tar -xz mv go $GOROOT - cd $GOROOT/src && GOARCH=386 ./make.bash + cd $GOROOT/src + GOARCH=386 ./make.bash - cd ~/go/src/github.com/junegunn/fzf/src && make install && cd .. && - yes | ./install && tmux + cd $FZF_BASE/src && make install && + cd $FZF_BASE/bin && ln -sf fzf-linux_amd64 fzf-$(./fzf --version)-linux_amd64 && + cd $FZF_BASE && yes | ./install && + tmux new "rake test > out && touch ok" && cat out && [ -e ok ] diff --git a/test/test_go.rb b/test/test_go.rb index de8fac7..a499a92 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -10,6 +10,7 @@ class Tmux def initialize shell = 'bash' @win = go("new-window -P -F '#I' 'bash --rcfile ~/.fzf.#{shell}'").first + @lines = `tput lines`.chomp.to_i end def self.current @@ -25,7 +26,7 @@ class Tmux end def close timeout = 1 - send_keys 'C-c', 'C-u', 'C-d' + send_keys 'C-c', 'C-u', 'exit', :Enter wait(timeout) { closed? } end @@ -41,7 +42,7 @@ class Tmux def capture go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}") raise "Window not found" if $?.exitstatus != 0 - File.read(TEMPNAME).split($/) + File.read(TEMPNAME).split($/)[0, @lines] end def until timeout = 1 @@ -54,7 +55,15 @@ private until yield waited += 0.1 sleep 0.1 - raise "timeout" if waited > timeout + if waited > timeout + hl = '=' * 10 + puts hl + capture.each_with_index do |line, idx| + puts [idx.to_s.rjust(2), line].join(': ') + end + puts hl + raise "timeout" + end end end @@ -85,7 +94,7 @@ class TestGoFZF < MiniTest::Unit::TestCase def test_vanilla tmux.send_keys "seq 1 100000 | fzf > #{tempname}", :Enter - tmux.until { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } + tmux.until(10) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } lines = tmux.capture assert_equal ' 2', lines[-4] assert_equal '> 1', lines[-3] @@ -94,7 +103,7 @@ class TestGoFZF < MiniTest::Unit::TestCase # Testing basic key bindings tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab' - tmux.until { |lines| lines.last == '> 391' } + tmux.until { |lines| lines[-2] == ' 856/100000' } lines = tmux.capture assert_equal '> 1391', lines[-4] assert_equal ' 391', lines[-3] @@ -117,7 +126,7 @@ class TestGoFZF < MiniTest::Unit::TestCase def test_fzf_prompt tmux.send_keys "fzf -q 'foo bar foo-bar'", :Enter - tmux.until { |lines| lines.last =~ /foo-bar/ } + tmux.until { |lines| lines.last =~ /^>/ } # CTRL-A tmux.send_keys "C-A", "(" From 1990f3c992b7825dce6c959e2b4f26429c148cc9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 10:51:39 +0900 Subject: [PATCH 059/408] Do not build i386 binary on Travis CI to speed up the process --- .travis.yml | 4 +--- src/Makefile | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9943ca6..d4196e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,10 +16,8 @@ script: | curl https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | tar -xz mv go $GOROOT - cd $GOROOT/src - GOARCH=386 ./make.bash - cd $FZF_BASE/src && make install && + cd $FZF_BASE/src && make test fzf/fzf-linux_amd64 install && cd $FZF_BASE/bin && ln -sf fzf-linux_amd64 fzf-$(./fzf --version)-linux_amd64 && cd $FZF_BASE && yes | ./install && tmux new "rake test > out && touch ok" && cat out && [ -e ok ] diff --git a/src/Makefile b/src/Makefile index eda0404..2604c3b 100644 --- a/src/Makefile +++ b/src/Makefile @@ -36,7 +36,7 @@ test: go get go test -v ./... -install: build $(BINDIR)/fzf +install: $(BINDIR)/fzf uninstall: rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64) From 0a83705d210ced8056a5ce503db50ebae2c2d44e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 10:57:07 +0900 Subject: [PATCH 060/408] Use Go 1.4.1 to build linux binaries --- src/Dockerfile.arch | 2 +- src/Dockerfile.centos | 2 +- src/Dockerfile.ubuntu | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch index 054b95c..e37a8b2 100644 --- a/src/Dockerfile.arch +++ b/src/Dockerfile.arch @@ -6,7 +6,7 @@ RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git # Install Go 1.4 RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ + https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 ENV GOPATH /go diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos index 5b27925..bbe065e 100644 --- a/src/Dockerfile.centos +++ b/src/Dockerfile.centos @@ -6,7 +6,7 @@ RUN yum install -y git gcc make tar ncurses-devel # Install Go 1.4 RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ + https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 ENV GOPATH /go diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu index 91bf780..9d28b32 100644 --- a/src/Dockerfile.ubuntu +++ b/src/Dockerfile.ubuntu @@ -7,7 +7,7 @@ RUN apt-get update && apt-get -y upgrade && \ # Install Go 1.4 RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ + https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 ENV GOPATH /go From 5c0dc79ffae4c2da24ff77c4aea370287491693d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 11:07:04 +0900 Subject: [PATCH 061/408] Print selected items in the order they are selected --- src/terminal.go | 44 +++++++++++++++++++++++++++++++++++--------- test/test_go.rb | 13 +++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index abd7069..49083f7 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -30,7 +30,7 @@ type Terminal struct { progress int reading bool merger *Merger - selected map[*string]*string + selected map[*string]selectedItem reqBox *util.EventBox eventBox *util.EventBox mutex sync.Mutex @@ -38,6 +38,25 @@ type Terminal struct { suppress bool } +type selectedItem struct { + at time.Time + text *string +} + +type ByTimeOrder []selectedItem + +func (a ByTimeOrder) Len() int { + return len(a) +} + +func (a ByTimeOrder) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByTimeOrder) Less(i, j int) bool { + return a[i].at.Before(a[j].at) +} + var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} const ( @@ -70,7 +89,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { multi: opts.Multi, printQuery: opts.PrintQuery, merger: EmptyMerger, - selected: make(map[*string]*string), + selected: make(map[*string]selectedItem), reqBox: util.NewEventBox(), eventBox: eventBox, mutex: sync.Mutex{}, @@ -139,12 +158,13 @@ func (t *Terminal) output() { fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString()) } } else { - for ptr, orig := range t.selected { - if orig != nil { - fmt.Println(*orig) - } else { - fmt.Println(*ptr) - } + sels := make([]selectedItem, 0, len(t.selected)) + for _, sel := range t.selected { + sels = append(sels, sel) + } + sort.Sort(ByTimeOrder(sels)) + for _, sel := range sels { + fmt.Println(*sel.text) } } } @@ -444,7 +464,13 @@ func (t *Terminal) Loop() { if idx < t.merger.Length() { item := t.merger.Get(idx) if _, found := t.selected[item.text]; !found { - t.selected[item.text] = item.origText + var strptr *string + if item.origText != nil { + strptr = item.origText + } else { + strptr = item.text + } + t.selected[item.text] = selectedItem{time.Now(), strptr} } else { delete(t.selected, item.text) } diff --git a/test/test_go.rb b/test/test_go.rb index a499a92..26d7a04 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -189,5 +189,18 @@ class TestGoFZF < MiniTest::Unit::TestCase tmux.until { |lines| lines.last !~ /^>/ } tmux.close end + + def test_fzf_multi_order + 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 + 'C-K', 'C-K', 'C-K', 'C-K', :BTab, :BTab, # 5, 6 + :PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7 + tmux.until { |lines| lines[-2].include? '(6)' } + tmux.send_keys "C-M" + assert_equal %w[3 2 5 6 8 7], File.read(tempname).split($/) + tmux.close + end end From 4c3ae847b6e134d39ddfdf466088f670ecb158f1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 11:20:00 +0900 Subject: [PATCH 062/408] Add test case for --with-nth + --multi --- test/test_go.rb | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 26d7a04..0b39ea8 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -124,7 +124,7 @@ class TestGoFZF < MiniTest::Unit::TestCase assert_equal 'hello', File.read(tempname).chomp end - def test_fzf_prompt + def test_key_bindings tmux.send_keys "fzf -q 'foo bar foo-bar'", :Enter tmux.until { |lines| lines.last =~ /^>/ } @@ -190,7 +190,7 @@ class TestGoFZF < MiniTest::Unit::TestCase tmux.close end - def test_fzf_multi_order + def test_multi_order tmux.send_keys "seq 1 10 | fzf --multi > #{tempname}", :Enter tmux.until { |lines| lines.last =~ /^>/ } @@ -202,5 +202,31 @@ class TestGoFZF < MiniTest::Unit::TestCase assert_equal %w[3 2 5 6 8 7], File.read(tempname).split($/) tmux.close end + + def test_with_nth + [true, false].each do |multi| + tmux.send_keys "(echo ' 1st 2nd 3rd/'; + echo ' first second third/') | + fzf #{"--multi" if multi} -x --nth 2 --with-nth 2,-1,1 > #{tempname}", + :Enter + tmux.until { |lines| lines[-2] && lines[-2].include?('2/2') } + + # Transformed list + lines = tmux.capture + assert_equal ' second third/first', lines[-4] + assert_equal '> 2nd 3rd/1st', lines[-3] + + # However, the output must not be transformed + if multi + tmux.send_keys :BTab, :BTab, :Enter + assert_equal [' 1st 2nd 3rd/', ' first second third/'], File.read(tempname).split($/) + else + tmux.send_keys '^', '3' + tmux.until { |lines| lines[-2].include?('1/2') } + tmux.send_keys :Enter + assert_equal [' 1st 2nd 3rd/'], File.read(tempname).split($/) + end + end + end end From fe89ac8a890b175377a4e613add6c61fbf6cce4c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 11:57:21 +0900 Subject: [PATCH 063/408] Add script for updating release assets --- src/update_assets.rb | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100755 src/update_assets.rb diff --git a/src/update_assets.rb b/src/update_assets.rb new file mode 100755 index 0000000..e9c750d --- /dev/null +++ b/src/update_assets.rb @@ -0,0 +1,41 @@ +#!/usr/bin/env ruby + +# http://www.rubydoc.info/github/rest-client/rest-client/RestClient +require 'rest_client' + +if ARGV.length < 3 + puts "usage: #$0 " +end + +token, version, *files = ARGV +base = "https://api.github.com/repos/junegunn/fzf-bin/releases" + +# List releases +rels = JSON.parse(RestClient.get(base, :authorization => "token #{token}")) +rel = rels.find { |r| r['tag_name'] == version } +unless rel + puts "#{version} not found" + exit 1 +end + +# List assets +assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }] + +files.select { |f| File.exists? f }.each do |file| + name = File.basename file + + if asset_id = assets[name] + puts "#{name} found. Deleting asset id #{asset_id}." + RestClient.delete "#{base}/assets/#{asset_id}", + :authorization => "token #{token}" + else + puts "#{name} not found" + end + + puts "Uploading #{name}" + RestClient.post( + "#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}", + File.read(file), + :authorization => "token #{token}", + :content_type => "application/octet-stream") +end From be5c17612a6996615fb4f38a4b373b577273c079 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 12:21:38 +0900 Subject: [PATCH 064/408] Add basic test case for --reverse --- test/test_go.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/test_go.rb b/test/test_go.rb index 0b39ea8..fd04efc 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -224,9 +224,26 @@ class TestGoFZF < MiniTest::Unit::TestCase tmux.send_keys '^', '3' tmux.until { |lines| lines[-2].include?('1/2') } tmux.send_keys :Enter + tmux.send_keys 'echo -n done', :Enter + tmux.until { |lines| lines[-1].include?('done') } assert_equal [' 1st 2nd 3rd/'], File.read(tempname).split($/) end end end + + def test_scroll + [true, false].each do |rev| + tmux.send_keys "seq 1 100 | fzf #{'--reverse' if rev} > #{tempname}", :Enter + tmux.until { |lines| rev ? lines.first == '>' : lines.last == '>' } + + 110.times do + tmux.send_keys rev ? :Down : :Up + end + tmux.send_keys :Enter + tmux.send_keys 'echo -n done', :Enter + tmux.until { |lines| lines[-1].include?('done') } + assert_equal '100', File.read(tempname).chomp + end + end end From c3a198d0c7ea000101ffde27b28d33c7f8b4230b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 12:37:24 +0900 Subject: [PATCH 065/408] Add test cases for --select-1 and --exit-0 --- test/test_go.rb | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index fd04efc..0476bea 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -3,6 +3,12 @@ require 'minitest/autorun' +class NilClass + def include? str + false + end +end + class Tmux TEMPNAME = '/tmp/fzf-test.txt' @@ -79,6 +85,19 @@ class TestGoFZF < MiniTest::Unit::TestCase '/tmp/output' 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' @@ -112,7 +131,7 @@ class TestGoFZF < MiniTest::Unit::TestCase tmux.send_keys :Enter tmux.close - assert_equal '1391', File.read(tempname).chomp + assert_equal '1391', readtemp.chomp end def test_fzf_default_command @@ -121,7 +140,7 @@ class TestGoFZF < MiniTest::Unit::TestCase tmux.send_keys :Enter tmux.close - assert_equal 'hello', File.read(tempname).chomp + assert_equal 'hello', readtemp.chomp end def test_key_bindings @@ -199,7 +218,7 @@ 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" - assert_equal %w[3 2 5 6 8 7], File.read(tempname).split($/) + assert_equal %w[3 2 5 6 8 7], readtemp.split($/) tmux.close end @@ -209,7 +228,7 @@ class TestGoFZF < MiniTest::Unit::TestCase echo ' first second third/') | fzf #{"--multi" if multi} -x --nth 2 --with-nth 2,-1,1 > #{tempname}", :Enter - tmux.until { |lines| lines[-2] && lines[-2].include?('2/2') } + tmux.until { |lines| lines[-2].include?('2/2') } # Transformed list lines = tmux.capture @@ -219,14 +238,14 @@ class TestGoFZF < MiniTest::Unit::TestCase # However, the output must not be transformed if multi tmux.send_keys :BTab, :BTab, :Enter - assert_equal [' 1st 2nd 3rd/', ' first second third/'], File.read(tempname).split($/) + assert_equal [' 1st 2nd 3rd/', ' first second third/'], readtemp.split($/) else tmux.send_keys '^', '3' tmux.until { |lines| lines[-2].include?('1/2') } tmux.send_keys :Enter tmux.send_keys 'echo -n done', :Enter tmux.until { |lines| lines[-1].include?('done') } - assert_equal [' 1st 2nd 3rd/'], File.read(tempname).split($/) + assert_equal [' 1st 2nd 3rd/'], readtemp.split($/) end end end @@ -242,8 +261,20 @@ class TestGoFZF < MiniTest::Unit::TestCase tmux.send_keys :Enter tmux.send_keys 'echo -n done', :Enter tmux.until { |lines| lines[-1].include?('done') } - assert_equal '100', File.read(tempname).chomp + assert_equal '100', readtemp.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($/) + 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($/) + end end From 3d39ab5ded72476e709581a4e818b254bab5d044 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 13:38:42 +0900 Subject: [PATCH 066/408] Fix flaky tests --- test/test_go.rb | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 0476bea..341f7f2 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -15,18 +15,10 @@ class Tmux attr_reader :win def initialize shell = 'bash' - @win = go("new-window -P -F '#I' 'bash --rcfile ~/.fzf.#{shell}'").first + @win = go("new-window -d -P -F '#I' 'PS1= bash --rcfile ~/.fzf.#{shell}'").first @lines = `tput lines`.chomp.to_i end - def self.current - `tmux display-message -p '#I'`.split($/).first - end - - def self.select id - system "tmux select-window -t #{id}" - end - def closed? !go("list-window -F '#I'").include?(win) end @@ -85,6 +77,12 @@ class TestGoFZF < MiniTest::Unit::TestCase '/tmp/output' end + def rmtemp + while File.exists? tempname + File.unlink tempname rescue nil + end + end + def readtemp waited = 0 while waited < 5 @@ -101,14 +99,12 @@ class TestGoFZF < MiniTest::Unit::TestCase def setup ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_COMMAND' - @prev = Tmux.current @tmux = Tmux.new - File.unlink tempname rescue nil + rmtemp end def teardown @tmux.kill - Tmux.select @prev end def test_vanilla @@ -210,7 +206,7 @@ class TestGoFZF < MiniTest::Unit::TestCase end def test_multi_order - tmux.send_keys "seq 1 10 | fzf --multi > #{tempname}", :Enter + tmux.send_keys "seq 1 10 | fzf --multi > #{tempname} && echo -n done", :Enter tmux.until { |lines| lines.last =~ /^>/ } tmux.send_keys :Tab, :Up, :Up, :Tab, :Tab, :Tab, # 3, 2 @@ -218,15 +214,18 @@ 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.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}", + fzf #{"--multi" if multi} -x --nth 2 --with-nth 2,-1,1 > #{tempname} && echo -n done", :Enter tmux.until { |lines| lines[-2].include?('2/2') } @@ -238,12 +237,12 @@ 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($/) else tmux.send_keys '^', '3' tmux.until { |lines| lines[-2].include?('1/2') } tmux.send_keys :Enter - tmux.send_keys 'echo -n done', :Enter tmux.until { |lines| lines[-1].include?('done') } assert_equal [' 1st 2nd 3rd/'], readtemp.split($/) end @@ -252,14 +251,13 @@ class TestGoFZF < MiniTest::Unit::TestCase def test_scroll [true, false].each do |rev| - tmux.send_keys "seq 1 100 | fzf #{'--reverse' if rev} > #{tempname}", :Enter - tmux.until { |lines| rev ? lines.first == '>' : lines.last == '>' } + rmtemp - 110.times do - tmux.send_keys rev ? :Down : :Up - end + 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 *110.times.map { rev ? :Down : :Up } + tmux.until { |lines| lines.include? '> 100' } tmux.send_keys :Enter - tmux.send_keys 'echo -n done', :Enter tmux.until { |lines| lines[-1].include?('done') } assert_equal '100', readtemp.chomp end From 4dcc0f10b8514d1c2fd67671ef83314fa73508e6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 13:45:56 +0900 Subject: [PATCH 067/408] Fix Travis CI build by ignoring trailing empty lines :sob: --- test/test_go.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 341f7f2..0de36be 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -40,7 +40,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] + File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse end def until timeout = 1 From 30f9651f9957d20dfb1b43ddc3ae77bae8c3ac59 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 14:15:26 +0900 Subject: [PATCH 068/408] 0.9.1 --- install | 2 +- src/constants.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install b/install index e03c357..edcad66 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.1-dev +version=0.9.1 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/src/constants.go b/src/constants.go index c3d8575..a1be9c4 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.1-dev" +const Version = "0.9.1" // fzf events const ( From a3101120fddd16eaef39e120d9e5678f46d28897 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Jan 2015 20:40:00 +0900 Subject: [PATCH 069/408] Update install script --- install | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/install b/install index edcad66..b016426 100755 --- a/install +++ b/install @@ -16,6 +16,7 @@ check_binary() { local output=$("$fzf_base"/bin/fzf --version 2>&1) if [ "$version" = "$output" ]; then echo "$output" + binary_error="" else echo "$output != $version" rm -f "$fzf_base"/bin/fzf @@ -27,8 +28,12 @@ check_binary() { symlink() { echo " - Creating symlink: bin/$1 -> bin/fzf" (cd "$fzf_base"/bin && - rm -f fzf + rm -f fzf && ln -sf $1 fzf) + if [ $? -ne 0 ]; then + binary_error="Failed to create symlink" + return 1 + fi } download() { @@ -37,8 +42,7 @@ download() { echo " - Already exists" check_binary && return elif [ -x "$fzf_base"/bin/$1 ]; then - symlink $1 - check_binary && return + symlink $1 && check_binary && return fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin if [ $? -ne 0 ]; then From 0a0955755a75ffffe4b37a74eb16cdd6e0fa3a1d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 18 Jan 2015 16:32:22 +0900 Subject: [PATCH 070/408] Add note on installation --- src/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/README.md b/src/README.md index 511b744..8ed8309 100644 --- a/src/README.md +++ b/src/README.md @@ -19,6 +19,9 @@ git pull ./install ``` +Otherwise, simply 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/ From 1b6cb3532d9c84e559b8a9fd584919c051d203b2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 18 Jan 2015 16:34:10 +0900 Subject: [PATCH 071/408] Update src/README.md --- src/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/README.md b/src/README.md index 8ed8309..18ccec3 100644 --- a/src/README.md +++ b/src/README.md @@ -19,8 +19,8 @@ git pull ./install ``` -Otherwise, simply follow [the instruction][install] as before. You can also -install fzf using Homebrew if you prefer that way. +Otherwise, follow [the instruction][install] as before. You can also install +fzf using Homebrew if you prefer that way. Motivations ----------- From 8bead4ae3427689955a1248619917f8beeba9585 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 18 Jan 2015 16:59:04 +0900 Subject: [PATCH 072/408] Improved handling of tab characters --- src/constants.go | 2 +- src/terminal.go | 72 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/constants.go b/src/constants.go index a1be9c4..c588f9f 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.1" +const Version = "0.9.2-dev" // fzf events const ( diff --git a/src/terminal.go b/src/terminal.go index 49083f7..e5cf64a 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1,10 +1,12 @@ package fzf import ( + "bytes" "fmt" "os" "regexp" "sort" + "strings" "sync" "time" @@ -169,10 +171,18 @@ func (t *Terminal) output() { } } +func runeWidth(r rune, prefixWidth int) int { + if r == '\t' { + return 8 - prefixWidth%8 + } else { + return runewidth.RuneWidth(r) + } +} + func displayWidth(runes []rune) int { l := 0 for _, r := range runes { - l += runewidth.RuneWidth(r) + l += runeWidth(r, l) } return l } @@ -254,16 +264,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 +292,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 +344,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() From c35d98dc42e8327ffbfd17701efa3d5fb68eeae0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 23 Jan 2015 06:26:00 +0900 Subject: [PATCH 073/408] Nullify --nth option when it's irrelevant --- src/options.go | 11 +++++++++++ src/options_test.go | 40 +++++++++++++++++++++++++++++++++++----- src/tokenizer.go | 22 ++++++++++++++++------ 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/options.go b/src/options.go index e1dba29..b11328e 100644 --- a/src/options.go +++ b/src/options.go @@ -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 diff --git a/src/options_test.go b/src/options_test.go index e10ec56..782ad79 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -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) + } + } + } +} diff --git a/src/tokenizer.go b/src/tokenizer.go index 26aebd9..d38f46f 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -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 { From 48e16edb474a59e74197793c2790f666d394e401 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 23 Jan 2015 20:30:50 +0900 Subject: [PATCH 074/408] Redraw and adjust upon terminal resize --- src/curses/curses.go | 4 ++++ src/terminal.go | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/curses/curses.go b/src/curses/curses.go index 8ebb583..454f1e3 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -421,6 +421,10 @@ func Clear() { C.clear() } +func Endwin() { + C.endwin() +} + func Refresh() { C.refresh() } diff --git a/src/terminal.go b/src/terminal.go index e5cf64a..e1eb4c2 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -4,10 +4,12 @@ import ( "bytes" "fmt" "os" + "os/signal" "regexp" "sort" "strings" "sync" + "syscall" "time" C "github.com/junegunn/fzf/src/curses" @@ -452,6 +454,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() { @@ -471,6 +482,8 @@ func (t *Terminal) Loop() { t.suppress = false case reqRedraw: C.Clear() + C.Endwin() + C.Refresh() t.printAll() case reqClose: C.Close() From a47c06cb612a392b8c1b9a5c92782510d48fa746 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 23 Jan 2015 20:32:56 +0900 Subject: [PATCH 075/408] Fix update_assets script --- src/update_assets.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/src/update_assets.rb b/src/update_assets.rb index e9c750d..c849f9e 100755 --- a/src/update_assets.rb +++ b/src/update_assets.rb @@ -5,6 +5,7 @@ require 'rest_client' if ARGV.length < 3 puts "usage: #$0 " + exit 1 end token, version, *files = ARGV From 71a7b3a26f344130736ca9d2e8f4210edffec257 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 24 Jan 2015 12:28:00 +0900 Subject: [PATCH 076/408] Improve rendering performance by caching rune widths Related: 8bead4a --- src/terminal.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index e1eb4c2..e9de686 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -62,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 @@ -176,8 +177,12 @@ 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 { - return runewidth.RuneWidth(r) + w := runewidth.RuneWidth(r) + _runeWidths[r] = w + return w } } From 1476fc7f3bff575ab6adda3d53b7e03e282d3119 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 24 Jan 2015 13:25:11 +0900 Subject: [PATCH 077/408] Refactor test code --- test/test_go.rb | 103 ++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 0de36be..0f055e2 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -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,40 @@ 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 end From 448132c46c172169af22583403993f4505727ba5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 24 Jan 2015 13:26:33 +0900 Subject: [PATCH 078/408] Fix error when --query contains wide-length characters --- src/terminal.go | 2 +- test/test_go.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index e9de686..cfed1a0 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -86,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{}, diff --git a/test/test_go.rb b/test/test_go.rb index 0f055e2..99c6962 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -273,5 +273,13 @@ class TestGoFZF < MiniTest::Unit::TestCase 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 From 9cb0cdb4aca2d0d71b619e1707cda9d69e224790 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 24 Jan 2015 14:49:21 +0900 Subject: [PATCH 079/408] 0.9.2 --- install | 2 +- src/constants.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install b/install index b016426..b0ec19b 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.1 +version=0.9.2 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/src/constants.go b/src/constants.go index c588f9f..9b52743 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.2-dev" +const Version = "0.9.2" // fzf events const ( From 53d5d9d1624d23175eca4c1e1bd93bbbb7b41b0c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 24 Jan 2015 18:07:24 +0100 Subject: [PATCH 080/408] Improve `find` command for cd widgets: exclude proc/dev etc When using the widget in "/", it would descend into 'dev/'. Using '*' for the starting path would do so also with the new '-fstype' excludes. `cut -b3-` and `sed 1d` have been added to massage the different format of the list. This also uses `-L` with all calls to find, especially for the file finders. Ref: https://github.com/junegunn/fzf/pull/122 --- install | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/install b/install index b0ec19b..07cb0ab 100755 --- a/install +++ b/install @@ -202,10 +202,10 @@ EOF # Key bindings # ------------ __fsel() { - command find * -path '*/\.*' -prune \ + command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | fzf -m | while read item; do + -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do printf '%q ' "$item" done echo @@ -226,7 +226,8 @@ __fsel_tmux() { __fcd() { local dir - dir=$(command find -L ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf +m) && printf 'cd %q' "$dir" + dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m) && printf 'cd %q' "$dir" } __use_tmux=0 @@ -274,17 +275,16 @@ unset __use_tmux fi EOFZF - else + else # zsh cat >> $src << "EOFZF" # Key bindings # ------------ # CTRL-T - Paste the selected file path(s) into the command line __fsel() { - set -o nonomatch - command find * -path '*/\.*' -prune \ + command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | fzf -m | while read item; do + -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do printf '%q ' "$item" done echo @@ -314,8 +314,8 @@ bindkey '^T' fzf-file-widget # ALT-C - cd into the selected directory fzf-cd-widget() { - cd "${$(set -o nonomatch; command find -L * -path '*/\.*' -prune \ - -o -type d -print 2> /dev/null | fzf):-.}" + cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m):-.}" zle reset-prompt } zle -N fzf-cd-widget @@ -369,14 +369,15 @@ function fzf_key_bindings end function __fzf_list - command find * -path '*/\.*' -prune \ + command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null + -o -type l -print 2> /dev/null | sed 1d | cut -b3- end function __fzf_list_dir - command find -L * -path '*/\.*' -prune -o -type d -print 2> /dev/null + command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \ + -prune -o -type d -print 2> /dev/null | sed 1d | cut -b3- end function __fzf_escape From 423317b82afeb0e4b92f24bdd6fe2c9238709f66 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 28 Jan 2015 01:18:20 +0900 Subject: [PATCH 081/408] Update README.md --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4b7c6c5..f863c63 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,13 @@ curl -L https://github.com/junegunn/fzf/archive/master.tar.gz | The script will setup: -- `fzf` function (bash, zsh, fish) +- `fzf` command - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) - Fuzzy auto-completion (bash) -If you don't use any of the aforementioned shells, you have to manually place -fzf executable in a directory included in `$PATH`. Key bindings and -auto-completion will not be available in that case. +If you don't need the extra goodies, you can simply download +[fzf executable](https://github.com/junegunn/fzf-bin/releases) +and place it in a directory included in `$PATH`. ### Install as Vim plugin @@ -542,4 +542,3 @@ Author ------ Junegunn Choi - From 9f39671e65b0b48752484696e41f3a7296fa5906 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 28 Jan 2015 01:45:34 +0900 Subject: [PATCH 082/408] Update README.md Update outdated --help output --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f863c63..8157bf0 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ usage: fzf [options] -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, --sort Sort the result +s, --no-sort Do not sort the result. Keep the sequence unchanged. Interface @@ -92,7 +92,7 @@ usage: fzf [options] 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_OPTS Defaults options. (e.g. "-x -m") ``` fzf will launch curses-based finder, read the list from STDIN, and write the From e6a0de409478f373c9d3da56f8e1b0270b688bb1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 29 Jan 2015 17:37:35 +0900 Subject: [PATCH 083/408] Fix Travis CI build --- .travis.yml | 1 + test/test_go.rb | 48 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index d4196e1..692ade7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: ruby install: +- sudo apt-get update - sudo apt-get install -y libncurses-dev lib32ncurses5-dev - sudo add-apt-repository -y ppa:pi-rho/dev - sudo apt-get update diff --git a/test/test_go.rb b/test/test_go.rb index 99c6962..70e531f 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -38,7 +38,7 @@ class Tmux attr_reader :win def initialize shell = 'bash' - @win = go("new-window -d -P -F '#I' 'PS1=FIN PROMPT_COMMAND= bash --rcfile ~/.fzf.#{shell}'").first + @win = go("new-window -d -P -F '#I' 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.#{shell}'").first @lines = `tput lines`.chomp.to_i end @@ -96,6 +96,7 @@ end class TestGoFZF < MiniTest::Unit::TestCase include Temp + FIN = 'FIN' TEMPNAME = '/tmp/output' attr_reader :tmux @@ -110,8 +111,23 @@ class TestGoFZF < MiniTest::Unit::TestCase @tmux.kill end + def fzf(*opts) + opts = opts.map { |o| + case o + when Symbol + o = o.to_s + o.length > 1 ? "--#{o.gsub('_', '-')}" : "-#{o}" + when String, Numeric + o.to_s + else + nil + end + }.compact + "fzf #{opts.join ' '} > #{TEMPNAME} && echo #{FIN}" + end + def test_vanilla - tmux.send_keys "seq 1 100000 | fzf > #{TEMPNAME}", :Enter + tmux.send_keys "seq 1 100000 | #{fzf}", :Enter tmux.until(10) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } lines = tmux.capture assert_equal ' 2', lines[-4] @@ -134,7 +150,7 @@ class TestGoFZF < MiniTest::Unit::TestCase 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}", :Enter tmux.until { |lines| lines.last =~ /^>/ } tmux.send_keys :Enter @@ -209,7 +225,7 @@ class TestGoFZF < MiniTest::Unit::TestCase end def test_multi_order - tmux.send_keys "seq 1 10 | fzf --multi > #{TEMPNAME}", :Enter + tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter tmux.until { |lines| lines.last =~ /^>/ } tmux.send_keys :Tab, :Up, :Up, :Tab, :Tab, :Tab, # 3, 2 @@ -217,7 +233,7 @@ 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?('FIN') } + tmux.until { |lines| lines[-1].include?(FIN) } assert_equal %w[3 2 5 6 8 7], readonce.split($/) tmux.close end @@ -226,7 +242,7 @@ class TestGoFZF < MiniTest::Unit::TestCase [true, false].each do |multi| tmux.send_keys "(echo ' 1st 2nd 3rd/'; echo ' first second third/') | - fzf #{"--multi" if multi} -x --nth 2 --with-nth 2,-1,1 > #{TEMPNAME}", + #{fzf multi && :multi, :x, :nth, 2, :with_nth, '2,-1,1'}", :Enter tmux.until { |lines| lines[-2].include?('2/2') } @@ -238,13 +254,13 @@ 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?('FIN') } + 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?('FIN') } + tmux.until { |lines| lines[-1].include?(FIN) } assert_equal [' 1st 2nd 3rd/'], readonce.split($/) end end @@ -252,33 +268,33 @@ class TestGoFZF < MiniTest::Unit::TestCase def test_scroll [true, false].each do |rev| - tmux.send_keys "seq 1 100 | fzf #{'--reverse' if rev} > #{TEMPNAME}", :Enter + tmux.send_keys "seq 1 100 | #{fzf rev && :reverse}", :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?('FIN') } + 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}", :Enter - tmux.until { |lines| lines[-1].include?('FIN') } + tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 5555, :'1'}", :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}", :Enter - tmux.until { |lines| lines[-1].include?('FIN') } + tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555555, :'0'}", :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.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter tmux.until { |lines| lines.last.start_with? '>' } tmux.send_keys :Enter - tmux.until { |lines| lines[-1].include?('FIN') } + tmux.until { |lines| lines[-1].include?(FIN) } assert_equal ['가나다'], readonce.split($/) end end From ff09c275d4abf21920526f3134869bda0a7464d5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 12 Feb 2015 10:13:49 +0900 Subject: [PATCH 084/408] Fix bash script when fzf_base contains spaces --- install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install b/install index 07cb0ab..a06693a 100755 --- a/install +++ b/install @@ -159,7 +159,7 @@ for shell in bash zsh; do echo -n "Generate ~/.fzf.$shell ... " src=~/.fzf.${shell} - fzf_completion="[[ \$- =~ i ]] && source $fzf_base/fzf-completion.${shell}" + fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/fzf-completion.${shell}\"" if [ $auto_completion -ne 0 ]; then fzf_completion="# $fzf_completion" fi From 6c2ce28d0dca357195d267406b90e705c35312d5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 13 Feb 2015 12:25:19 +0900 Subject: [PATCH 085/408] Add `--sync` option --- README.md | 4 +++- src/constants.go | 2 +- src/core.go | 19 +++++++------------ src/options.go | 12 +++++++++++- src/util/eventbox.go | 15 +++++++++++++++ 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8157bf0..fb62dff 100644 --- a/README.md +++ b/README.md @@ -89,10 +89,12 @@ usage: fzf [options] -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 + --sync Synchronous search for multi-staged filtering + (e.g. 'fzf --multi | fzf --sync') Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m") + FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m') ``` fzf will launch curses-based finder, read the list from STDIN, and write the diff --git a/src/constants.go b/src/constants.go index 9b52743..7d54223 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.2" +const Version = "0.9.3" // fzf events const ( diff --git a/src/core.go b/src/core.go index ee90413..ab61a6e 100644 --- a/src/core.go +++ b/src/core.go @@ -110,19 +110,8 @@ func Run(options *Options) { } pattern := patternBuilder([]rune(patternString)) - looping := true eventBox.Unwatch(EvtReadNew) - for looping { - eventBox.Wait(func(events *util.Events) { - for evt := range *events { - switch evt { - case EvtReadFin: - looping = false - return - } - } - }) - } + eventBox.WaitFor(EvtReadFin) snapshot, _ := chunkList.Snapshot() merger, cancelled := matcher.scan(MatchRequest{ @@ -142,6 +131,12 @@ func Run(options *Options) { } } + // Synchronous search + if opts.Sync { + eventBox.Unwatch(EvtReadNew) + eventBox.WaitFor(EvtReadFin) + } + // Go interactive go matcher.Loop() diff --git a/src/options.go b/src/options.go index b11328e..c426e77 100644 --- a/src/options.go +++ b/src/options.go @@ -41,10 +41,12 @@ const usage = `usage: fzf [options] -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 + --sync Synchronous search for multi-staged filtering + (e.g. 'fzf --multi | fzf --sync') Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m") + FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m') ` @@ -88,6 +90,7 @@ type Options struct { Exit0 bool Filter *string PrintQuery bool + Sync bool Version bool } @@ -111,6 +114,7 @@ func defaultOptions() *Options { Exit0: false, Filter: nil, PrintQuery: false, + Sync: false, Version: false} } @@ -244,6 +248,12 @@ func parseOptions(opts *Options, allArgs []string) { opts.PrintQuery = false case "--prompt": opts.Prompt = nextString(allArgs, &i, "prompt string required") + case "--sync": + opts.Sync = true + case "--no-sync": + opts.Sync = false + case "--async": + opts.Sync = false case "--version": opts.Version = true default: diff --git a/src/util/eventbox.go b/src/util/eventbox.go index 568ad9f..a5821ea 100644 --- a/src/util/eventbox.go +++ b/src/util/eventbox.go @@ -78,3 +78,18 @@ func (b *EventBox) Unwatch(events ...EventType) { b.ignore[event] = true } } + +func (b *EventBox) WaitFor(event EventType) { + looping := true + for looping { + b.Wait(func(events *Events) { + for evt := range *events { + switch evt { + case event: + looping = false + return + } + } + }) + } +} From 1b5599972abb1ca4d1498ec1dfe9bc041f8d238a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 17 Feb 2015 13:15:16 +0900 Subject: [PATCH 086/408] Update installation instruction --- README.md | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index fb62dff..527d5d7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,20 @@ the likes. Installation ------------ +fzf project consists of the followings: + +- `fzf` executable +- Shell extensions + - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) + - Fuzzy auto-completion (bash) + +You can [download fzf executable][bin] alone, but it's recommended that you +install the extra stuff using the attached install script. + +[bin]: https://github.com/junegunn/fzf-bin/releases + +### Using git (recommended) + Clone this repository and run [install](https://github.com/junegunn/fzf/blob/master/install) script. @@ -19,6 +33,8 @@ git clone https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install ``` +### Using curl + In case you don't have git installed: ```sh @@ -28,15 +44,16 @@ curl -L https://github.com/junegunn/fzf/archive/master.tar.gz | ~/.fzf/install ``` -The script will setup: +### Using Homebrew -- `fzf` command -- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) -- Fuzzy auto-completion (bash) +On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. -If you don't need the extra goodies, you can simply download -[fzf executable](https://github.com/junegunn/fzf-bin/releases) -and place it in a directory included in `$PATH`. +```sh +brew install fzf + +# Install shell extensions +/usr/local/Cellar/fzf/0.9.2/install +``` ### Install as Vim plugin @@ -46,8 +63,7 @@ Once you have cloned the repository, add the following line to your .vimrc. set rtp+=~/.fzf ``` -Or you may use [vim-plug](https://github.com/junegunn/vim-plug) to manage fzf -inside Vim: +Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf: ```vim Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } From d760b790b37a5d26b3068ed9f5b4efcaa3cf5cae Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 17 Feb 2015 19:28:10 +0900 Subject: [PATCH 087/408] Fix typo in code --- src/matcher.go | 2 +- src/reader_test.go | 8 ++++---- src/util/eventbox.go | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/matcher.go b/src/matcher.go index 1ea9541..bbd9c89 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -183,7 +183,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { break } - if !empty && m.reqBox.Peak(reqReset) { + if !empty && m.reqBox.Peek(reqReset) { return nil, wait() } diff --git a/src/reader_test.go b/src/reader_test.go index 5800b3f..144a3ff 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -14,7 +14,7 @@ func TestReadFromCommand(t *testing.T) { eventBox: eb} // Check EventBox - if eb.Peak(EvtReadNew) { + if eb.Peek(EvtReadNew) { t.Error("EvtReadNew should not be set yet") } @@ -25,7 +25,7 @@ func TestReadFromCommand(t *testing.T) { } // Check EventBox again - if !eb.Peak(EvtReadNew) { + if !eb.Peek(EvtReadNew) { t.Error("EvtReadNew should be set yet") } @@ -38,7 +38,7 @@ func TestReadFromCommand(t *testing.T) { }) // EventBox is cleared - if eb.Peak(EvtReadNew) { + if eb.Peek(EvtReadNew) { t.Error("EvtReadNew should not be set yet") } @@ -50,7 +50,7 @@ func TestReadFromCommand(t *testing.T) { } // Check EventBox again - if eb.Peak(EvtReadNew) { + if eb.Peek(EvtReadNew) { t.Error("Command failed. EvtReadNew should be set") } } diff --git a/src/util/eventbox.go b/src/util/eventbox.go index a5821ea..e16b736 100644 --- a/src/util/eventbox.go +++ b/src/util/eventbox.go @@ -53,8 +53,8 @@ func (events *Events) Clear() { } } -// Peak peaks at the event box if the given event is set -func (b *EventBox) Peak(event EventType) bool { +// Peek peeks at the event box if the given event is set +func (b *EventBox) Peek(event EventType) bool { b.cond.L.Lock() defer b.cond.L.Unlock() _, ok := b.events[event] From e808151c287513c9aef35be339b10a2861115ade Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 Feb 2015 00:08:17 +0900 Subject: [PATCH 088/408] Make --select-1 and --exit-0 asynchronous --- src/core.go | 60 ++++++++++++++++++++++++++++--------------------- src/matcher.go | 8 ++----- src/terminal.go | 3 +++ 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/core.go b/src/core.go index ab61a6e..71e3aa9 100644 --- a/src/core.go +++ b/src/core.go @@ -95,40 +95,25 @@ func Run(options *Options) { } matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox) - // Defered-interactive / Non-interactive - // --select-1 | --exit-0 | --filter - if filtering := opts.Filter != nil; filtering || opts.Select1 || opts.Exit0 { - limit := 0 - var patternString string - if filtering { - patternString = *opts.Filter - } else { - if opts.Select1 || opts.Exit0 { - limit = 1 - } - patternString = opts.Query - } - pattern := patternBuilder([]rune(patternString)) + // Filtering mode + if opts.Filter != nil { + pattern := patternBuilder([]rune(*opts.Filter)) eventBox.Unwatch(EvtReadNew) eventBox.WaitFor(EvtReadFin) snapshot, _ := chunkList.Snapshot() - merger, cancelled := matcher.scan(MatchRequest{ + merger, _ := matcher.scan(MatchRequest{ chunks: snapshot, - pattern: pattern}, limit) + pattern: pattern}) - if !cancelled && (filtering || - opts.Exit0 && merger.Length() == 0 || - opts.Select1 && merger.Length() == 1) { - if opts.PrintQuery { - fmt.Println(patternString) - } - for i := 0; i < merger.Length(); i++ { - fmt.Println(merger.Get(i).AsString()) - } - os.Exit(0) + if opts.PrintQuery { + fmt.Println(*opts.Filter) } + for i := 0; i < merger.Length(); i++ { + fmt.Println(merger.Get(i).AsString()) + } + os.Exit(0) } // Synchronous search @@ -142,7 +127,11 @@ func Run(options *Options) { // Terminal I/O terminal := NewTerminal(opts, eventBox) + deferred := opts.Select1 || opts.Exit0 go terminal.Loop() + if !deferred { + terminal.startChan <- true + } // Event coordination reading := true @@ -176,6 +165,25 @@ func Run(options *Options) { case EvtSearchFin: switch val := value.(type) { case *Merger: + if deferred { + count := val.Length() + if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 { + deferred = false + terminal.startChan <- true + } else if !reading { + if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { + if opts.PrintQuery { + fmt.Println(opts.Query) + } + for i := 0; i < count; i++ { + fmt.Println(val.Get(i).AsString()) + } + os.Exit(0) + } + deferred = false + terminal.startChan <- true + } + } terminal.UpdateList(val) } } diff --git a/src/matcher.go b/src/matcher.go index bbd9c89..84130b2 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -86,7 +86,7 @@ func (m *Matcher) Loop() { } if !foundCache { - merger, cancelled = m.scan(request, 0) + merger, cancelled = m.scan(request) } if !cancelled { @@ -121,7 +121,7 @@ type partialResult struct { matches []*Item } -func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { +func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { startedAt := time.Now() numChunks := len(request.chunks) @@ -175,10 +175,6 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { count++ matchCount += matchesInChunk - if limit > 0 && matchCount > limit { - return nil, wait() // For --select-1 and --exit-0 - } - if count == numChunks { break } diff --git a/src/terminal.go b/src/terminal.go index cfed1a0..3d914ac 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -40,6 +40,7 @@ type Terminal struct { mutex sync.Mutex initFunc func() suppress bool + startChan chan bool } type selectedItem struct { @@ -99,6 +100,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { eventBox: eventBox, mutex: sync.Mutex{}, suppress: true, + startChan: make(chan bool, 1), initFunc: func() { C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse) }} @@ -446,6 +448,7 @@ func (t *Terminal) rubout(pattern string) { // Loop is called to start Terminal I/O func (t *Terminal) Loop() { + <-t.startChan { // Late initialization t.mutex.Lock() t.initFunc() From 95e5beb34e8baa324a3bee59e2705c3c942b5427 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 Feb 2015 00:22:17 +0900 Subject: [PATCH 089/408] Update Homebrew instruction --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 527d5d7..16cedbf 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,8 @@ On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. ```sh brew install fzf -# Install shell extensions -/usr/local/Cellar/fzf/0.9.2/install +# Install shell extensions - this should be done whenever fzf is updated +/usr/local/Cellar/fzf/$(fzf --version)/install ``` ### Install as Vim plugin From f5b034095a74ae88410bf3383c39b1e99c0f36a0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 Feb 2015 00:51:44 +0900 Subject: [PATCH 090/408] Fix race condition in asynchronous -1 and -0 --- src/core.go | 6 +++--- src/matcher.go | 6 ++++-- src/merger.go | 2 ++ test/test_go.rb | 10 ++++++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/core.go b/src/core.go index 71e3aa9..ea97b4e 100644 --- a/src/core.go +++ b/src/core.go @@ -149,11 +149,11 @@ func Run(options *Options) { reading = reading && evt == EvtReadNew snapshot, count := chunkList.Snapshot() terminal.UpdateCount(count, !reading) - matcher.Reset(snapshot, terminal.Input(), false) + matcher.Reset(snapshot, terminal.Input(), false, !reading) case EvtSearchNew: snapshot, _ := chunkList.Snapshot() - matcher.Reset(snapshot, terminal.Input(), true) + matcher.Reset(snapshot, terminal.Input(), true, !reading) delay = false case EvtSearchProgress: @@ -170,7 +170,7 @@ func Run(options *Options) { if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 { deferred = false terminal.startChan <- true - } else if !reading { + } else if val.final { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.PrintQuery { fmt.Println(opts.Query) diff --git a/src/matcher.go b/src/matcher.go index 84130b2..bfe9d28 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -14,6 +14,7 @@ import ( type MatchRequest struct { chunks []*Chunk pattern *Pattern + final bool } // Matcher is responsible for performing search @@ -91,6 +92,7 @@ func (m *Matcher) Loop() { if !cancelled { m.mergerCache[patternString] = merger + merger.final = request.final m.eventBox.Set(EvtSearchFin, merger) } } @@ -197,7 +199,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } // Reset is called to interrupt/signal the ongoing search -func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { +func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool) { pattern := m.patternBuilder(patternRunes) var event util.EventType @@ -206,5 +208,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { } else { event = reqRetry } - m.reqBox.Set(event, MatchRequest{chunks, pattern}) + m.reqBox.Set(event, MatchRequest{chunks, pattern, final}) } diff --git a/src/merger.go b/src/merger.go index bd2158d..5bfc81d 100644 --- a/src/merger.go +++ b/src/merger.go @@ -12,6 +12,7 @@ type Merger struct { merged []*Item cursors []int sorted bool + final bool count int } @@ -22,6 +23,7 @@ func NewMerger(lists [][]*Item, sorted bool) *Merger { merged: []*Item{}, cursors: make([]int, len(lists)), sorted: sorted, + final: false, count: 0} for _, list := range mg.lists { diff --git a/test/test_go.rb b/test/test_go.rb index 70e531f..27c7b33 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -290,6 +290,16 @@ class TestGoFZF < MiniTest::Unit::TestCase assert_equal ['555555'], readonce.split($/) end + def test_select_1_exit_0_fail + [:'0', :'1', [:'1', :'0']].each do |opt| + tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter + tmux.until { |lines| lines.last =~ /^> 5/ } + tmux.send_keys :BTab, :BTab, :BTab, :Enter + tmux.until { |lines| lines[-1].include?(FIN) } + assert_equal ['5', '5', '15', '25'], readonce.split($/) + end + end + def test_query_unicode tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter tmux.until { |lines| lines.last.start_with? '>' } From 5667667d1faf61d7c10b02fc57c4f0a32e9226f7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 Feb 2015 12:07:54 +0900 Subject: [PATCH 091/408] Add test case for --sync option --- test/test_go.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 27c7b33..fe32a4e 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -112,6 +112,10 @@ class TestGoFZF < MiniTest::Unit::TestCase end def fzf(*opts) + fzf!(*opts) + " > #{TEMPNAME} && echo #{FIN}" + end + + def fzf!(*opts) opts = opts.map { |o| case o when Symbol @@ -123,7 +127,7 @@ class TestGoFZF < MiniTest::Unit::TestCase nil end }.compact - "fzf #{opts.join ' '} > #{TEMPNAME} && echo #{FIN}" + "fzf #{opts.join ' '}" end def test_vanilla @@ -307,5 +311,16 @@ class TestGoFZF < MiniTest::Unit::TestCase tmux.until { |lines| lines[-1].include?(FIN) } assert_equal ['가나다'], readonce.split($/) end + + def test_sync + tmux.send_keys "seq 1 100 | #{fzf! :multi} | awk '{print \\$1 \\$1}' | #{fzf :sync}", :Enter + tmux.until { |lines| lines[-1] == '>' } + tmux.send_keys 9 + tmux.until { |lines| lines[-2] == ' 19/100' } + tmux.send_keys :BTab, :BTab, :BTab, :Enter + tmux.until { |lines| lines[-1] == '>' } + tmux.send_keys 'C-K', :Enter + assert_equal ['1919'], readonce.split($/) + end end From b9b1eeffce10819bcf9bd966a6459ac961c32c07 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 Feb 2015 12:12:59 +0900 Subject: [PATCH 092/408] Update Vader tests --- test/fzf.vader | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/fzf.vader b/test/fzf.vader index 5d40142..74dbde1 100644 --- a/test/fzf.vader +++ b/test/fzf.vader @@ -7,16 +7,16 @@ Execute (fzf#run with dir option): AssertEqual ['fzf.vader'], result let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir })) - AssertEqual ['fzf.vader', 'test_fzf.rb'], result + AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result Execute (fzf#run with Funcref command): let g:ret = [] - function! g:proc(e) + function! g:FzfTest(e) call add(g:ret, a:e) endfunction - let result = sort(fzf#run({ 'sink': function('g:proc'), 'options': '--filter e', 'dir': g:dir })) - AssertEqual ['fzf.vader', 'test_fzf.rb'], result - AssertEqual ['fzf.vader', 'test_fzf.rb'], sort(g:ret) + let result = sort(fzf#run({ 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir })) + AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result + AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], sort(g:ret) Execute (fzf#run with string source): let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' })) From 4a1752d3fc7f069b0f8afb12ed625acb6fd2aee2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 Feb 2015 13:19:20 +0900 Subject: [PATCH 093/408] 0.9.3 --- install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install b/install index a06693a..5cd0672 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.2 +version=0.9.3 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) From c1aa5c5f3380315621d30d99b258667775b0fad3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 26 Feb 2015 01:42:15 +0900 Subject: [PATCH 094/408] Add --tac option and reverse display order of --no-sort DISCLAIMER: This is a backward incompatible change --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ README.md | 16 +++++----------- install | 10 +++++----- src/constants.go | 2 +- src/core.go | 2 +- src/item.go | 27 +++++++++++++++++++++------ src/item_test.go | 15 +++++++++++---- src/matcher.go | 12 +++++++++--- src/merger.go | 34 +++++++++++++++++++--------------- src/merger_test.go | 6 +++--- src/options.go | 13 ++++++++++--- src/terminal.go | 20 +++++--------------- test/test_go.rb | 27 +++++++++++++++++++++++++-- test/test_ruby.rb | 2 +- 14 files changed, 148 insertions(+), 70 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..914a582 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +CHANGELOG +========= + +0.9.4 +----- + +#### New features + +- Added `--tac` option to reverse the order of the input. + - One might argue that this option is unnecessary since we can already put + `tac` or `tail -r` in the command pipeline to achieve the same result. + However, the advantage of `--tac` is that it does not block until the + input is complete. + +#### *Backward incompatible changes* + +- `--no-sort` option will no longer reverse the display order. You may want to + use the new `--tac` option with `--no-sort`. +``` +history | fzf +s --tac +``` + +0.9.3 +----- + +#### New features +- Added `--sync` option for multi-staged filtering + +#### Improvements +- `--select-1` and `--exit-0` will start finder immediately when the condition + cannot be met + diff --git a/README.md b/README.md index 16cedbf..e5ada36 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Usage ``` usage: fzf [options] - Search + Search mode -x, --extended Extended-search mode -e, --extended-exact Extended-search mode (exact match) -i Case-insensitive match (default: smart-case match) @@ -87,8 +87,9 @@ usage: fzf [options] -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) Search result - -s, --sort Sort the result - +s, --no-sort Do not sort the result. Keep the sequence unchanged. + +s, --no-sort Do not sort the result + --tac Reverse the order of the input + (e.g. 'history | fzf --tac --no-sort') Interface -m, --multi Enable multi-select with tab/shift-tab @@ -128,13 +129,6 @@ files excluding hidden ones. (You can override the default command with vim $(fzf) ``` -If you want to preserve the exact sequence of the input, provide `--no-sort` (or -`+s`) option. - -```sh -history | fzf +s -``` - ### Keys Use CTRL-J and CTRL-K (or CTRL-N and CTRL-P) to change the selection, press @@ -197,7 +191,7 @@ fd() { # fh - repeat history fh() { - eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s | sed 's/ *[0-9]* *//') + eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s --tac | sed 's/ *[0-9]* *//') } # fkill - kill process diff --git a/install b/install index 5cd0672..8b53f72 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.3 +version=0.9.4 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) @@ -245,7 +245,7 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then fi # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"' + bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"' # ALT-C - cd into the selected directory bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' @@ -263,7 +263,7 @@ else bind -m vi-command '"\C-t": "i\C-t"' # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' + bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' bind -m vi-command '"\C-r": "i\C-r"' # ALT-C - cd into the selected directory @@ -323,7 +323,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { - LBUFFER=$(fc -l 1 | fzf +s +m -n2..,.. | sed "s/ *[0-9*]* *//") + LBUFFER=$(fc -l 1 | fzf +s --tac +m -n2..,.. | sed "s/ *[0-9*]* *//") zle redisplay } zle -N fzf-history-widget @@ -412,7 +412,7 @@ function fzf_key_bindings end function __fzf_ctrl_r - history | __fzf_reverse | fzf +s +m > $TMPDIR/fzf.result + history | __fzf_reverse | fzf +s --tac +m > $TMPDIR/fzf.result and commandline (cat $TMPDIR/fzf.result) commandline -f repaint rm -f $TMPDIR/fzf.result diff --git a/src/constants.go b/src/constants.go index 7d54223..f513853 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.3" +const Version = "0.9.4" // fzf events const ( diff --git a/src/core.go b/src/core.go index ea97b4e..ec4c5e8 100644 --- a/src/core.go +++ b/src/core.go @@ -93,7 +93,7 @@ func Run(options *Options) { return BuildPattern( opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) } - matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox) + matcher := NewMatcher(patternBuilder, opts.Sort > 0, opts.Tac, eventBox) // Filtering mode if opts.Filter != nil { diff --git a/src/item.go b/src/item.go index 4cbd3f9..2b8a9d1 100644 --- a/src/item.go +++ b/src/item.go @@ -87,10 +87,28 @@ func (a ByRelevance) Less(i, j int) bool { irank := a[i].Rank(true) jrank := a[j].Rank(true) - return compareRanks(irank, jrank) + return compareRanks(irank, jrank, false) } -func compareRanks(irank Rank, jrank Rank) bool { +// ByRelevanceTac is for sorting Items +type ByRelevanceTac []*Item + +func (a ByRelevanceTac) Len() int { + return len(a) +} + +func (a ByRelevanceTac) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByRelevanceTac) Less(i, j int) bool { + irank := a[i].Rank(true) + jrank := a[j].Rank(true) + + return compareRanks(irank, jrank, true) +} + +func compareRanks(irank Rank, jrank Rank, tac bool) bool { if irank.matchlen < jrank.matchlen { return true } else if irank.matchlen > jrank.matchlen { @@ -103,8 +121,5 @@ func compareRanks(irank Rank, jrank Rank) bool { return false } - if irank.index <= jrank.index { - return true - } - return false + return (irank.index <= jrank.index) != tac } diff --git a/src/item_test.go b/src/item_test.go index 0e83631..372ab4a 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -20,12 +20,19 @@ func TestOffsetSort(t *testing.T) { } func TestRankComparison(t *testing.T) { - if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}) || - !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}) || - !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}) || - !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}) { + if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, false) || + !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) || + !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, false) || + !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) { t.Error("Invalid order") } + + if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, true) || + !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) || + !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, true) || + !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) { + t.Error("Invalid order (tac)") + } } // Match length, string length, index diff --git a/src/matcher.go b/src/matcher.go index bfe9d28..0879a08 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -21,6 +21,7 @@ type MatchRequest struct { type Matcher struct { patternBuilder func([]rune) *Pattern sort bool + tac bool eventBox *util.EventBox reqBox *util.EventBox partitions int @@ -38,10 +39,11 @@ const ( // NewMatcher returns a new Matcher func NewMatcher(patternBuilder func([]rune) *Pattern, - sort bool, eventBox *util.EventBox) *Matcher { + sort bool, tac bool, eventBox *util.EventBox) *Matcher { return &Matcher{ patternBuilder: patternBuilder, sort: sort, + tac: tac, eventBox: eventBox, reqBox: util.NewEventBox(), partitions: runtime.NumCPU(), @@ -159,7 +161,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { countChan <- len(matches) } if !empty && m.sort { - sort.Sort(ByRelevance(sliceMatches)) + if m.tac { + sort.Sort(ByRelevanceTac(sliceMatches)) + } else { + sort.Sort(ByRelevance(sliceMatches)) + } } resultChan <- partialResult{idx, sliceMatches} }(idx, chunks) @@ -195,7 +201,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches } - return NewMerger(partialResults, !empty && m.sort), false + return NewMerger(partialResults, !empty && m.sort, m.tac), false } // Reset is called to interrupt/signal the ongoing search diff --git a/src/merger.go b/src/merger.go index 5bfc81d..41323c1 100644 --- a/src/merger.go +++ b/src/merger.go @@ -3,7 +3,7 @@ package fzf import "fmt" // Merger with no data -var EmptyMerger = NewMerger([][]*Item{}, false) +var EmptyMerger = NewMerger([][]*Item{}, false, false) // Merger holds a set of locally sorted lists of items and provides the view of // a single, globally-sorted list @@ -12,17 +12,19 @@ type Merger struct { merged []*Item cursors []int sorted bool + tac bool final bool count int } // NewMerger returns a new Merger -func NewMerger(lists [][]*Item, sorted bool) *Merger { +func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger { mg := Merger{ lists: lists, merged: []*Item{}, cursors: make([]int, len(lists)), sorted: sorted, + tac: tac, final: false, count: 0} @@ -39,19 +41,21 @@ func (mg *Merger) Length() int { // Get returns the pointer to the Item object indexed by the given integer func (mg *Merger) Get(idx int) *Item { - if len(mg.lists) == 1 { - return mg.lists[0][idx] - } else if !mg.sorted { - for _, list := range mg.lists { - numItems := len(list) - if idx < numItems { - return list[idx] - } - idx -= numItems - } - panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) + if mg.sorted { + return mg.mergedGet(idx) } - return mg.mergedGet(idx) + + if mg.tac { + idx = mg.Length() - idx - 1 + } + for _, list := range mg.lists { + numItems := len(list) + if idx < numItems { + return list[idx] + } + idx -= numItems + } + panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) } func (mg *Merger) mergedGet(idx int) *Item { @@ -66,7 +70,7 @@ func (mg *Merger) mergedGet(idx int) *Item { } if cursor >= 0 { rank := list[cursor].Rank(false) - if minIdx < 0 || compareRanks(rank, minRank) { + if minIdx < 0 || compareRanks(rank, minRank, mg.tac) { minRank = rank minIdx = listIdx } diff --git a/src/merger_test.go b/src/merger_test.go index f79da09..b69d633 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -62,7 +62,7 @@ func TestMergerUnsorted(t *testing.T) { cnt := len(items) // Not sorted: same order - mg := NewMerger(lists, false) + mg := NewMerger(lists, false, false) assert(t, cnt == mg.Length(), "Invalid Length") for i := 0; i < cnt; i++ { assert(t, items[i] == mg.Get(i), "Invalid Get") @@ -74,7 +74,7 @@ func TestMergerSorted(t *testing.T) { cnt := len(items) // Sorted sorted order - mg := NewMerger(lists, true) + mg := NewMerger(lists, true, false) assert(t, cnt == mg.Length(), "Invalid Length") sort.Sort(ByRelevance(items)) for i := 0; i < cnt; i++ { @@ -84,7 +84,7 @@ func TestMergerSorted(t *testing.T) { } // Inverse order - mg2 := NewMerger(lists, true) + mg2 := NewMerger(lists, true, false) for i := cnt - 1; i >= 0; i-- { if items[i] != mg2.Get(i) { t.Error("Not sorted", items[i], mg2.Get(i)) diff --git a/src/options.go b/src/options.go index c426e77..dc8f0b8 100644 --- a/src/options.go +++ b/src/options.go @@ -11,7 +11,7 @@ import ( const usage = `usage: fzf [options] - Search + Search mode -x, --extended Extended-search mode -e, --extended-exact Extended-search mode (exact match) -i Case-insensitive match (default: smart-case match) @@ -23,8 +23,9 @@ const usage = `usage: fzf [options] -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) Search result - -s, --sort Sort the result - +s, --no-sort Do not sort the result. Keep the sequence unchanged. + +s, --no-sort Do not sort the result + --tac Reverse the order of the input + (e.g. 'history | fzf --tac --no-sort') Interface -m, --multi Enable multi-select with tab/shift-tab @@ -78,6 +79,7 @@ type Options struct { WithNth []Range Delimiter *regexp.Regexp Sort int + Tac bool Multi bool Mouse bool Color bool @@ -102,6 +104,7 @@ func defaultOptions() *Options { WithNth: make([]Range, 0), Delimiter: nil, Sort: 1000, + Tac: false, Multi: false, Mouse: true, Color: true, @@ -212,6 +215,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Sort = optionalNumeric(allArgs, &i) case "+s", "--no-sort": opts.Sort = 0 + case "--tac": + opts.Tac = true + case "--no-tac": + opts.Tac = false case "-i": opts.Case = CaseIgnore case "+i": diff --git a/src/terminal.go b/src/terminal.go index 3d914ac..bd426d1 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -22,7 +22,6 @@ import ( type Terminal struct { prompt string reverse bool - tac bool cx int cy int offset int @@ -85,7 +84,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) return &Terminal{ prompt: opts.Prompt, - tac: opts.Sort == 0, reverse: opts.Reverse, cx: len(input), cy: 0, @@ -148,13 +146,6 @@ func (t *Terminal) UpdateList(merger *Merger) { t.reqBox.Set(reqList, nil) } -func (t *Terminal) listIndex(y int) int { - if t.tac { - return t.merger.Length() - y - 1 - } - return y -} - func (t *Terminal) output() { if t.printQuery { fmt.Println(string(t.input)) @@ -162,7 +153,7 @@ func (t *Terminal) output() { if len(t.selected) == 0 { cnt := t.merger.Length() if cnt > 0 && cnt > t.cy { - fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString()) + fmt.Println(t.merger.Get(t.cy).AsString()) } } else { sels := make([]selectedItem, 0, len(t.selected)) @@ -246,7 +237,7 @@ func (t *Terminal) printList() { for i := 0; i < maxy; i++ { t.move(i+2, 0, true) if i < count { - t.printItem(t.merger.Get(t.listIndex(i+t.offset)), i == t.cy-t.offset) + t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset) } } } @@ -525,9 +516,8 @@ func (t *Terminal) Loop() { } } toggle := func() { - idx := t.listIndex(t.cy) - if idx < t.merger.Length() { - item := t.merger.Get(idx) + if t.cy < t.merger.Length() { + item := t.merger.Get(t.cy) if _, found := t.selected[item.text]; !found { var strptr *string if item.origText != nil { @@ -650,7 +640,7 @@ func (t *Terminal) Loop() { } else if me.Double { // Double-click if my >= 2 { - if t.vset(my-2) && t.listIndex(t.cy) < t.merger.Length() { + if t.vset(my-2) && t.cy < t.merger.Length() { req(reqClose) } } diff --git a/test/test_go.rb b/test/test_go.rb index fe32a4e..889ace4 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -15,7 +15,7 @@ module Temp waited = 0 while waited < 5 begin - data = File.read(name) + data = `cat #{name}` return data unless data.empty? rescue sleep 0.1 @@ -93,7 +93,7 @@ private end end -class TestGoFZF < MiniTest::Unit::TestCase +class TestGoFZF < Minitest::Test include Temp FIN = 'FIN' @@ -322,5 +322,28 @@ class TestGoFZF < MiniTest::Unit::TestCase tmux.send_keys 'C-K', :Enter assert_equal ['1919'], readonce.split($/) end + + def test_tac + tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter + tmux.until { |lines| lines[-2].include? '1000/1000' } + tmux.send_keys :BTab, :BTab, :BTab, :Enter + assert_equal %w[1000 999 998], readonce.split($/) + end + + def test_tac_sort + tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter + tmux.until { |lines| lines[-2].include? '1000/1000' } + tmux.send_keys '99' + tmux.send_keys :BTab, :BTab, :BTab, :Enter + assert_equal %w[99 999 998], readonce.split($/) + end + + def test_tac_nosort + tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter + tmux.until { |lines| lines[-2].include? '1000/1000' } + tmux.send_keys '00' + tmux.send_keys :BTab, :BTab, :BTab, :Enter + assert_equal %w[1000 900 800], readonce.split($/) + end end diff --git a/test/test_ruby.rb b/test/test_ruby.rb index 674ed3b..25f923b 100644 --- a/test/test_ruby.rb +++ b/test/test_ruby.rb @@ -54,7 +54,7 @@ class MockTTY end end -class TestRubyFZF < MiniTest::Unit::TestCase +class TestRubyFZF < Minitest::Test def setup ENV.delete 'FZF_DEFAULT_SORT' ENV.delete 'FZF_DEFAULT_OPTS' From 4d2d18649c1740defed24ee67915062398d5a699 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 1 Mar 2015 03:00:36 +0900 Subject: [PATCH 095/408] Add basic test cases for shell extensions (#83) - Key bindings for bash, zsh, and fish - Fuzzy completion for bash (file, dir, process) --- .travis.yml | 4 + test/test_go.rb | 258 ++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 241 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 692ade7..6908677 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,15 @@ language: ruby +rvm: +- 2.2.0 install: - sudo apt-get update - sudo apt-get install -y libncurses-dev lib32ncurses5-dev - sudo add-apt-repository -y ppa:pi-rho/dev +- sudo apt-add-repository -y ppa:fish-shell/release-2 - sudo apt-get update - sudo apt-get install -y tmux=1.9a-1~ppa1~p +- sudo apt-get install -y zsh fish script: | export GOROOT=~/go1.4 diff --git a/test/test_go.rb b/test/test_go.rb index 889ace4..6aa438b 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2,11 +2,20 @@ # encoding: utf-8 require 'minitest/autorun' +require 'fileutils' class NilClass def include? str false end + + def start_with? str + false + end + + def end_with? str + false + end end module Temp @@ -30,6 +39,20 @@ module Temp end end +class Shell + class << self + def bash + 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash' + end + + def zsh + FileUtils.mkdir_p '/tmp/fzf-zsh' + FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc' + 'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh' + end + end +end + class Tmux include Temp @@ -37,18 +60,33 @@ class Tmux attr_reader :win - def initialize shell = 'bash' - @win = go("new-window -d -P -F '#I' 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.#{shell}'").first + def initialize shell = :bash + @win = + case shell + when :bash + go("new-window -d -P -F '#I' '#{Shell.bash}'").first + when :zsh + go("new-window -d -P -F '#I' '#{Shell.zsh}'").first + when :fish + go("new-window -d -P -F '#I' 'fish'").first + else + raise "Unknown shell: #{shell}" + end @lines = `tput lines`.chomp.to_i + + if shell == :fish + send_keys('function fish_prompt; end; clear', :Enter) + self.until { |lines| lines.empty? } + end end def closed? !go("list-window -F '#I'").include?(win) end - def close timeout = 1 + def close send_keys 'C-c', 'C-u', 'exit', :Enter - wait(timeout) { closed? } + wait { closed? } end def kill @@ -56,35 +94,68 @@ class Tmux end def send_keys *args + target = + if args.last.is_a?(Hash) + hash = args.pop + go("select-window -t #{win}") + "#{win}.#{hash[:pane]}" + else + win + end args = args.map { |a| %{"#{a}"} }.join ' ' - go("send-keys -t #{win} #{args}") + go("send-keys -t #{target} #{args}") end - def capture - go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}") - raise "Window not found" if $?.exitstatus != 0 + def capture opts = {} + timeout, pane = defaults(opts).values_at(:timeout, :pane) + waited = 0 + loop do + go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME}") + break if $?.exitstatus == 0 + + if waited > timeout + raise "Window not found" + end + waited += 0.1 + sleep 0.1 + end readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse end - def until timeout = 1 - wait(timeout) { yield capture } + def until opts = {} + lines = nil + wait(opts) do + yield lines = capture(opts) + end + lines end + def prepare + self.send_keys 'echo hello', :Enter + self.until { |lines| lines[-1].start_with?('hello') } + self.send_keys 'clear', :Enter + self.until { |lines| lines.empty? } + end private - def wait timeout = 1 + def defaults opts + { timeout: 5, pane: 0 }.merge(opts) + end + + def wait opts = {} + timeout, pane = defaults(opts).values_at(:timeout, :pane) waited = 0 until yield - waited += 0.1 - sleep 0.1 if waited > timeout hl = '=' * 10 puts hl - capture.each_with_index do |line, idx| + capture(opts).each_with_index do |line, idx| puts [idx.to_s.rjust(2), line].join(': ') end puts hl raise "timeout" end + waited += 0.1 + sleep 0.1 end end @@ -93,7 +164,7 @@ private end end -class TestGoFZF < Minitest::Test +class TestBase < Minitest::Test include Temp FIN = 'FIN' @@ -104,11 +175,6 @@ class TestGoFZF < Minitest::Test def setup ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_COMMAND' - @tmux = Tmux.new - end - - def teardown - @tmux.kill end def fzf(*opts) @@ -129,10 +195,22 @@ class TestGoFZF < Minitest::Test }.compact "fzf #{opts.join ' '}" end +end + +class TestGoFZF < TestBase + def setup + super + @tmux = Tmux.new + end + + def teardown + @tmux.kill + end def test_vanilla tmux.send_keys "seq 1 100000 | #{fzf}", :Enter - tmux.until(10) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } + tmux.until(timeout: 10) { |lines| + lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } lines = tmux.capture assert_equal ' 2', lines[-4] assert_equal '> 1', lines[-3] @@ -347,3 +425,141 @@ class TestGoFZF < Minitest::Test end end +module TestShell + def setup + super + end + + def teardown + @tmux.kill + end + + def test_ctrl_t + tmux.prepare + tmux.send_keys 'C-t', pane: 0 + lines = tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } + expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') + tmux.send_keys :BTab, :BTab, :Enter, pane: 1 + tmux.until(pane: 0) { |lines| lines[-1].include? expected } + tmux.send_keys 'C-c' + + # FZF_TMUX=0 + new_shell + tmux.send_keys 'C-t', pane: 0 + lines = tmux.until(pane: 0) { |lines| lines[-1].start_with? '>' } + expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') + tmux.send_keys :BTab, :BTab, :Enter, pane: 0 + tmux.until(pane: 0) { |lines| lines[-1].include? expected } + tmux.send_keys 'C-c', 'C-d' + end + + def test_alt_c + tmux.prepare + tmux.send_keys :Escape, :c + lines = tmux.until { |lines| lines[-1].start_with? '>' } + expected = lines[-3][2..-1] + p expected + tmux.send_keys :Enter + tmux.prepare + tmux.send_keys :pwd, :Enter + tmux.until { |lines| p lines; lines[-1].end_with?(expected) } + end + + def test_ctrl_r + tmux.prepare + tmux.send_keys 'echo 1st', :Enter; tmux.prepare + tmux.send_keys 'echo 2nd', :Enter; tmux.prepare + tmux.send_keys 'echo 3d', :Enter; tmux.prepare + tmux.send_keys 'echo 3rd', :Enter; tmux.prepare + tmux.send_keys 'echo 4th', :Enter; tmux.prepare + tmux.send_keys 'C-r' + tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys '3d' + tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort + tmux.send_keys :Enter + tmux.until { |lines| lines[-1] == 'echo 3rd' } + tmux.send_keys :Enter + tmux.until { |lines| lines[-1] == '3rd' } + end +end + +class TestBash < TestBase + include TestShell + + def new_shell + tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter + tmux.prepare + end + + def setup + super + @tmux = Tmux.new :bash + end + + def test_file_completion + tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter + tmux.prepare + tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab + tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys :BTab, :BTab, :Enter + tmux.until { |lines| + lines[-1].include?('/tmp/fzf-test/10') && + lines[-1].include?('/tmp/fzf-test/100') + } + end + + def test_dir_completion + tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}', :Enter + tmux.prepare + tmux.send_keys 'cd /tmp/fzf-test/**', :Tab + tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys :BTab, :BTab # BTab does not work here + tmux.send_keys 55 + tmux.until { |lines| lines[-2].start_with? ' 1/' } + tmux.send_keys :Enter + tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55' } + end + + def test_process_completion + tmux.send_keys 'sleep 12345 &', :Enter + lines = tmux.until { |lines| lines[-1].start_with? '[1]' } + pid = lines[-1].split.last + tmux.prepare + tmux.send_keys 'kill ', :Tab + tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys 'sleep12345' + tmux.until { |lines| lines[-3].include? 'sleep 12345' } + tmux.send_keys :Enter + tmux.until { |lines| lines[-1] == "kill #{pid}" } + end +end + +class TestZsh < TestBase + include TestShell + + def new_shell + tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter + tmux.prepare + end + + def setup + super + @tmux = Tmux.new :zsh + end +end + +class TestFish < TestBase + include TestShell + + def new_shell + tmux.send_keys 'env FZF_TMUX=0 fish', :Enter + tmux.send_keys 'function fish_prompt; end; clear', :Enter + tmux.until { |lines| lines.empty? } + end + + def setup + super + @tmux = Tmux.new :fish + end +end + From 94e8e6419f29c8a9a5998abca15e7aa70c7eabda Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 1 Mar 2015 11:16:38 +0900 Subject: [PATCH 096/408] Make --filter non-blocking when --no-sort (#132) When fzf works in filtering mode (--filter) and sorting is disabled (--no-sort), there's no need to block until input is complete. This commit makes fzf print the matches on-the-fly when the following condition is met: --filter FILTER --no-sort [--no-tac --no-sync] or simply: -f FILTER +s This removes unnecessary delay in use cases like the following: fzf -f xxx +s | head -5 However, in this case, fzf processes the input lines sequentially, so it cannot utilize multiple cores, which makes it slightly slower than the previous mode of execution where filtering is done in parallel after the entire input is loaded. If the user is concerned about the performance problem, one can add --sync option to re-enable buffering. --- src/core.go | 42 ++++++++++++++++-------- src/pattern.go | 79 ++++++++++++++++++++++++++------------------- src/pattern_test.go | 5 +-- 3 files changed, 76 insertions(+), 50 deletions(-) diff --git a/src/core.go b/src/core.go index ec4c5e8..62190d0 100644 --- a/src/core.go +++ b/src/core.go @@ -85,8 +85,11 @@ func Run(options *Options) { } // Reader - reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} - go reader.ReadSource() + streamingFilter := opts.Filter != nil && opts.Sort == 0 && !opts.Tac && !opts.Sync + if !streamingFilter { + reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} + go reader.ReadSource() + } // Matcher patternBuilder := func(runes []rune) *Pattern { @@ -97,21 +100,32 @@ func Run(options *Options) { // Filtering mode if opts.Filter != nil { - pattern := patternBuilder([]rune(*opts.Filter)) - - eventBox.Unwatch(EvtReadNew) - eventBox.WaitFor(EvtReadFin) - - snapshot, _ := chunkList.Snapshot() - merger, _ := matcher.scan(MatchRequest{ - chunks: snapshot, - pattern: pattern}) - if opts.PrintQuery { fmt.Println(*opts.Filter) } - for i := 0; i < merger.Length(); i++ { - fmt.Println(merger.Get(i).AsString()) + + pattern := patternBuilder([]rune(*opts.Filter)) + + if streamingFilter { + reader := Reader{ + func(str string) { + item := chunkList.trans(&str, 0) + if pattern.MatchItem(item) { + fmt.Println(*item.text) + } + }, eventBox} + reader.ReadSource() + } else { + eventBox.Unwatch(EvtReadNew) + eventBox.WaitFor(EvtReadFin) + + snapshot, _ := chunkList.Snapshot() + merger, _ := matcher.scan(MatchRequest{ + chunks: snapshot, + pattern: pattern}) + for i := 0; i < merger.Length(); i++ { + fmt.Println(merger.Get(i).AsString()) + } } os.Exit(0) } diff --git a/src/pattern.go b/src/pattern.go index 17e3b6b..725ce2d 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -219,12 +219,7 @@ Loop: } } - var matches []*Item - if p.mode == ModeFuzzy { - matches = p.fuzzyMatch(space) - } else { - matches = p.extendedMatch(space) - } + matches := p.matchChunk(space) if !p.hasInvTerm { _cache.Add(chunk, cacheKey, matches) @@ -232,6 +227,35 @@ Loop: return matches } +func (p *Pattern) matchChunk(chunk *Chunk) []*Item { + matches := []*Item{} + if p.mode == ModeFuzzy { + for _, item := range *chunk { + if sidx, eidx := p.fuzzyMatch(item); sidx >= 0 { + matches = append(matches, + dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}})) + } + } + } else { + for _, item := range *chunk { + if offsets := p.extendedMatch(item); len(offsets) == len(p.terms) { + matches = append(matches, dupItem(item, offsets)) + } + } + } + return matches +} + +// MatchItem returns true if the Item is a match +func (p *Pattern) MatchItem(item *Item) bool { + if p.mode == ModeFuzzy { + sidx, _ := p.fuzzyMatch(item) + return sidx >= 0 + } + offsets := p.extendedMatch(item) + return len(offsets) == len(p.terms) +} + func dupItem(item *Item, offsets []Offset) *Item { sort.Sort(ByOrder(offsets)) return &Item{ @@ -243,39 +267,26 @@ func dupItem(item *Item, offsets []Offset) *Item { rank: Rank{0, 0, item.index}} } -func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { - matches := []*Item{} - for _, item := range *chunk { - input := p.prepareInput(item) - if sidx, eidx := p.iter(algo.FuzzyMatch, input, p.text); sidx >= 0 { - matches = append(matches, - dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}})) - } - } - return matches +func (p *Pattern) fuzzyMatch(item *Item) (int, int) { + input := p.prepareInput(item) + return p.iter(algo.FuzzyMatch, input, p.text) } -func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { - matches := []*Item{} - for _, item := range *chunk { - input := p.prepareInput(item) - offsets := []Offset{} - for _, term := range p.terms { - pfun := p.procFun[term.typ] - if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { - if term.inv { - break - } - offsets = append(offsets, Offset{int32(sidx), int32(eidx)}) - } else if term.inv { - offsets = append(offsets, Offset{0, 0}) +func (p *Pattern) extendedMatch(item *Item) []Offset { + input := p.prepareInput(item) + offsets := []Offset{} + for _, term := range p.terms { + pfun := p.procFun[term.typ] + if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { + if term.inv { + break } - } - if len(offsets) == len(p.terms) { - matches = append(matches, dupItem(item, offsets)) + offsets = append(offsets, Offset{int32(sidx), int32(eidx)}) + } else if term.inv { + offsets = append(offsets, Offset{0, 0}) } } - return matches + return offsets } func (p *Pattern) prepareInput(item *Item) *Transformed { diff --git a/src/pattern_test.go b/src/pattern_test.go index 4d36eda..67542f2 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -98,14 +98,15 @@ func TestOrigTextAndTransformed(t *testing.T) { tokens := Tokenize(strptr("junegunn"), nil) trans := Transform(tokens, []Range{Range{1, 1}}) - for _, fun := range []func(*Chunk) []*Item{pattern.fuzzyMatch, pattern.extendedMatch} { + for _, mode := range []Mode{ModeFuzzy, ModeExtended} { chunk := Chunk{ &Item{ text: strptr("junegunn"), origText: strptr("junegunn.choi"), transformed: trans}, } - matches := fun(&chunk) + pattern.mode = mode + matches := pattern.matchChunk(&chunk) if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || matches[0].transformed != trans { From fe09559ee9e1f4c3b7273baacff7e12f12c9bd89 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 1 Mar 2015 11:49:11 +0900 Subject: [PATCH 097/408] Build with Go 1.4.2 --- src/Dockerfile.arch | 2 +- src/Dockerfile.centos | 2 +- src/Dockerfile.ubuntu | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch index e37a8b2..b5fd7c0 100644 --- a/src/Dockerfile.arch +++ b/src/Dockerfile.arch @@ -6,7 +6,7 @@ RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git # Install Go 1.4 RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \ + https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 ENV GOPATH /go diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos index bbe065e..c03f43a 100644 --- a/src/Dockerfile.centos +++ b/src/Dockerfile.centos @@ -6,7 +6,7 @@ RUN yum install -y git gcc make tar ncurses-devel # Install Go 1.4 RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \ + https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 ENV GOPATH /go diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu index 9d28b32..4778a6d 100644 --- a/src/Dockerfile.ubuntu +++ b/src/Dockerfile.ubuntu @@ -7,7 +7,7 @@ RUN apt-get update && apt-get -y upgrade && \ # Install Go 1.4 RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \ + https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 ENV GOPATH /go From b15a0e9650febf4b89e56cef82dce626a1ce74a8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 1 Mar 2015 12:31:49 +0900 Subject: [PATCH 098/408] Update CHANGELOG --- CHANGELOG.md | 50 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 914a582..b9e8e77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,29 +4,57 @@ CHANGELOG 0.9.4 ----- -#### New features +### New features -- Added `--tac` option to reverse the order of the input. - - One might argue that this option is unnecessary since we can already put - `tac` or `tail -r` in the command pipeline to achieve the same result. - However, the advantage of `--tac` is that it does not block until the - input is complete. +#### Added `--tac` option to reverse the order of the input. -#### *Backward incompatible changes* +One might argue that this option is unnecessary since we can already put `tac` +or `tail -r` in the command pipeline to achieve the same result. However, the +advantage of `--tac` is that it does not block until the input is complete. + +### *Backward incompatible changes* + +#### Changed behavior on `--no-sort` + +`--no-sort` option will no longer reverse the display order within finder. You +may want to use the new `--tac` option with `--no-sort`. -- `--no-sort` option will no longer reverse the display order. You may want to - use the new `--tac` option with `--no-sort`. ``` history | fzf +s --tac ``` +### Improvements + +#### `--filter` will not block when sort is disabled + +When fzf works in filtering mode (`--filter`) and sort is disabled +(`--no-sort`), there's no need to block until input is complete. The new +version of fzf will print the matches on-the-fly when the following condition +is met: + + --filter TERM --no-sort [--no-tac --no-sync] + +or simply: + + -f TERM +s + +This change removes unnecessary delay in the use cases like the following: + + fzf -f xxx +s | head -5 + +However, in this case, fzf processes the lines sequentially, so it cannot +utilize multiple cores, and fzf will run slightly slower than the previous +mode of execution where filtering is done in parallel after the entire input +is loaded. If the user is concerned about this performance problem, one can +add `--sync` option to re-enable buffering. + 0.9.3 ----- -#### New features +### New features - Added `--sync` option for multi-staged filtering -#### Improvements +### Improvements - `--select-1` and `--exit-0` will start finder immediately when the condition cannot be met From 79c147ed783dab85b03eeb8f5f1458b20c5d6315 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 4 Mar 2015 12:59:23 +0900 Subject: [PATCH 099/408] Fix #135 - Directory completion to append / --- fzf-completion.bash | 11 ++++++----- test/test_go.rb | 4 +++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/fzf-completion.bash b/fzf-completion.bash index 61797b1..b90206c 100644 --- a/fzf-completion.bash +++ b/fzf-completion.bash @@ -88,7 +88,7 @@ _fzf_path_completion() { [ "$dir" = './' ] && dir='' tput sc matches=$(find -L "$dir"* $1 2> /dev/null | fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do - printf '%q ' "$item" + printf "%q$3 " "$item" done) matches=${matches% } if [ -n "$matches" ]; then @@ -103,6 +103,7 @@ _fzf_path_completion() { [[ "$dir" =~ /$ ]] || dir="$dir"/ done else + shift shift shift _fzf_handle_dynamic_completion "$cmd" "$@" @@ -136,19 +137,19 @@ _fzf_list_completion() { _fzf_all_completion() { _fzf_path_completion \ "-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \ - "-m" "$@" + "-m" "" "$@" } _fzf_file_completion() { _fzf_path_completion \ "-name .git -prune -o -name .svn -prune -o -type f -print -o -type l -print" \ - "-m" "$@" + "-m" "" "$@" } _fzf_dir_completion() { _fzf_path_completion \ "-name .git -prune -o -name .svn -prune -o -type d -print" \ - "" "$@" + "" "/" "$@" } _fzf_kill_completion() { @@ -219,7 +220,7 @@ fi # Directory for cmd in $d_cmds; do - complete -F _fzf_dir_completion -o default -o bashdefault $cmd + complete -F _fzf_dir_completion -o nospace -o default -o bashdefault $cmd done # File diff --git a/test/test_go.rb b/test/test_go.rb index 6aa438b..680edd6 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -517,7 +517,9 @@ class TestBash < TestBase tmux.send_keys 55 tmux.until { |lines| lines[-2].start_with? ' 1/' } tmux.send_keys :Enter - tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55' } + tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' } + tmux.send_keys :xx + tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } end def test_process_completion From 7e2c18a1f63583fdc6086d707a785e7c53a34290 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 4 Mar 2015 13:03:54 +0900 Subject: [PATCH 100/408] Fix directory completion matching regular files Related: #135 --- fzf-completion.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fzf-completion.bash b/fzf-completion.bash index b90206c..6643ffe 100644 --- a/fzf-completion.bash +++ b/fzf-completion.bash @@ -220,7 +220,7 @@ fi # Directory for cmd in $d_cmds; do - complete -F _fzf_dir_completion -o nospace -o default -o bashdefault $cmd + complete -F _fzf_dir_completion -o nospace -o plusdirs $cmd done # File From 55828f389abed5721d025786b399854e78607e3b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 4 Mar 2015 13:13:11 +0900 Subject: [PATCH 101/408] Add test case for 7e2c18a --- test/test_go.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 680edd6..524e436 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -509,7 +509,7 @@ class TestBash < TestBase end def test_dir_completion - tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}', :Enter + tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter tmux.prepare tmux.send_keys 'cd /tmp/fzf-test/**', :Tab tmux.until { |lines| lines[-1].start_with? '>' } @@ -520,6 +520,16 @@ class TestBash < TestBase tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' } tmux.send_keys :xx tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } + + # Should not match regular files + tmux.send_keys :Tab + tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } + + # Fail back to plusdirs + tmux.send_keys :BSpace, :BSpace, :BSpace + tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55' } + tmux.send_keys :Tab + tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' } end def test_process_completion From 13044280037e693970683629374d1019b5598ac7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 6 Mar 2015 10:42:38 +0900 Subject: [PATCH 102/408] Update bash completion *for* fzf --- fzf-completion.bash | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/fzf-completion.bash b/fzf-completion.bash index 6643ffe..e609d2b 100644 --- a/fzf-completion.bash +++ b/fzf-completion.bash @@ -14,17 +14,17 @@ _fzf_orig_completion_filter() { } _fzf_opts_completion() { - local cur prev opts + local cur opts COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" opts=" -x --extended -e --extended-exact -i +i -n --nth -d --delimiter - -s --sort +s + +s --no-sort + --tac -m --multi --no-mouse +c --no-color @@ -36,14 +36,8 @@ _fzf_opts_completion() { -1 --select-1 -0 --exit-0 -f --filter - --print-query" - - case "${prev}" in - --sort|-s) - COMPREPLY=( $(compgen -W "$(seq 2000 1000 10000)" -- ${cur}) ) - return 0 - ;; - esac + --print-query + --sync" if [[ ${cur} =~ ^-|\+ ]]; then COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) From 5f3326a888f8a2afc3f163a705462c58fe591412 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 6 Mar 2015 13:21:55 +0900 Subject: [PATCH 103/408] Deprecation alert --- fzf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fzf b/fzf index 3b049b9..c60cc9f 100755 --- a/fzf +++ b/fzf @@ -8,6 +8,8 @@ # /_/ /___/_/ Fuzzy finder for your shell # # Version: 0.8.9 (Dec 24, 2014) +# Deprecation alert: +# This script is no longer maintained. Use the new Go version. # # Author: Junegunn Choi # URL: https://github.com/junegunn/fzf From e4ce64d10b766e94a26e53b63199ee0dd66648b3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 6 Mar 2015 18:51:50 +0900 Subject: [PATCH 104/408] Add fzf-tmux script --- bin/fzf-tmux | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100755 bin/fzf-tmux diff --git a/bin/fzf-tmux b/bin/fzf-tmux new file mode 100755 index 0000000..02d935e --- /dev/null +++ b/bin/fzf-tmux @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# fzf-tmux: starts fzf in a tmux split +# usage: fzf-tmux [-h HEIGHT[%]] [-w WIDTH[%]] [--] [FZF OPTIONS] + +args=() +opt="" +while [ $# -gt 0 ]; do + arg="$1" + case "$arg" in + -w*|-h*) + if [ ${#arg} -gt 2 ]; then + size="${arg:2}" + else + shift + size="$1" + fi + [[ "$arg" =~ ^-w ]] && opt="-h" + [[ "$size" =~ %$ ]] && opt="$opt -p ${size:0:-1}" || + opt="$opt -l $size" + ;; + --) + # "--" can be used to separate fzf-tmux options from fzf options to + # avoid conflicts + break + ;; + *) + args+=("$1") + ;; + esac + shift +done + +if [ -z "$TMUX_PANE" ]; then + fzf "${args[@]}" + exit $? +fi + +set -e + +# Build arguments to fzf +[ ${#args[@]} -gt 0 ] && fzf_args=$(printf '\\"%s\\" ' "${args[@]}"; echo '') + +# Clean up named pipes on exit +id=$RANDOM +fifo1=/tmp/fzf-fifo1-$id +fifo2=/tmp/fzf-fifo2-$id +fifo3=/tmp/fzf-fifo3-$id +cleanup() { + rm -f $fifo1 $fifo2 $fifo3 +} +trap cleanup EXIT SIGINT SIGTERM + +mkfifo $fifo2 +mkfifo $fifo3 +if [ -t 0 ]; then + tmux split-window $opt 'bash -c "fzf '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3'"' +else + mkfifo $fifo1 + tmux split-window $opt 'bash -c "fzf '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3'"' + cat <&0 > $fifo1 & +fi +cat $fifo2 +[ "$(cat $fifo3)" = '0' ] + From 07e2bd673e333568e150747a7d65dcacbd9ae1ca Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 6 Mar 2015 18:57:36 +0900 Subject: [PATCH 105/408] Update README --- README.md | 179 +++++++++++++++++------------------------------------- 1 file changed, 56 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index e5ada36..c7b79d3 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,17 @@ Installation fzf project consists of the followings: - `fzf` executable +- `fzf-tmux` script for launching fzf in a tmux split - Shell extensions - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) - - Fuzzy auto-completion (bash) + - Fuzzy auto-completion (bash only) You can [download fzf executable][bin] alone, but it's recommended that you install the extra stuff using the attached install script. [bin]: https://github.com/junegunn/fzf-bin/releases -### Using git (recommended) +#### Using git (recommended) Clone this repository and run [install](https://github.com/junegunn/fzf/blob/master/install) script. @@ -33,7 +34,7 @@ git clone https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install ``` -### Using curl +#### Using curl In case you don't have git installed: @@ -44,7 +45,7 @@ curl -L https://github.com/junegunn/fzf/archive/master.tar.gz | ~/.fzf/install ``` -### Using Homebrew +#### Using Homebrew On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. @@ -55,7 +56,7 @@ brew install fzf /usr/local/Cellar/fzf/$(fzf --version)/install ``` -### Install as Vim plugin +#### Install as Vim plugin Once you have cloned the repository, add the following line to your .vimrc. @@ -63,7 +64,8 @@ Once you have cloned the repository, add the following line to your .vimrc. set rtp+=~/.fzf ``` -Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf: +Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf +(recommended): ```vim Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } @@ -72,48 +74,6 @@ Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } Usage ----- -``` -usage: fzf [options] - - Search mode - -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, --no-sort Do not sort the result - --tac Reverse the order of the input - (e.g. 'history | fzf --tac --no-sort') - - 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: '> ') - - 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 - --sync Synchronous search for multi-staged filtering - (e.g. 'fzf --multi | fzf --sync') - - Environment variables - FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m') -``` - fzf will launch curses-based finder, read the list from STDIN, and write the selected item to STDOUT. @@ -129,27 +89,16 @@ files excluding hidden ones. (You can override the default command with vim $(fzf) ``` -### Keys +#### Using the finder -Use CTRL-J and CTRL-K (or CTRL-N and CTRL-P) to change the selection, press -enter key to select the item. CTRL-C, CTRL-G, or ESC will terminate the finder. +- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P)` to move cursor up and down +- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit +- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items +- Emacs style key bindings +- Mouse: scroll, click, double-click; shift-click and shift-scroll on + multi-select mode -The following readline key bindings should also work as expected. - -- CTRL-A / CTRL-E -- CTRL-B / CTRL-F -- CTRL-H / CTRL-D -- CTRL-W / CTRL-U / CTRL-Y -- ALT-B / ALT-F - -If you enable multi-select mode with `-m` option, you can select multiple items -with TAB or Shift-TAB key. - -You can also use mouse. Double-click on an item to select it or shift-click (or -ctrl-click) to select multiple items. Use mouse wheel to move the cursor up and -down. - -### Extended-search mode +#### Extended-search mode With `-x` or `--extended` option, fzf will start in "extended-search mode". @@ -168,40 +117,12 @@ such as: `^music .mp3$ sbtrkt !rmx` If you don't need fuzzy matching and do not wish to "quote" every word, start fzf with `-e` or `--extended-exact` option. -Useful examples ---------------- +Examples +-------- -```sh -# 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" --select-1 --exit-0) - [ -n "$file" ] && ${EDITOR:-vim} "$file" -} - -# fd - cd to selected directory -fd() { - local dir - dir=$(find ${1:-*} -path '*/\.*' -prune \ - -o -type d -print 2> /dev/null | fzf +m) && - cd "$dir" -} - -# fh - repeat history -fh() { - eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s --tac | sed 's/ *[0-9]* *//') -} - -# fkill - kill process -fkill() { - ps -ef | sed 1d | fzf -m | awk '{print $2}' | xargs kill -${1:-9} -} -``` - -For more examples, see [the wiki -page](https://github.com/junegunn/fzf/wiki/examples). +Many useful examples can be found on [the wiki +page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your +own as well. Key bindings for command line ----------------------------- @@ -225,13 +146,26 @@ If you want to customize the key bindings, consider editing the installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and `~/.config/fish/functions/fzf_key_bindings.fish`. -Auto-completion ---------------- +`fzf-tmux` script +----------------- -Disclaimer: *Auto-completion feature is currently experimental, it can change -over time* +[fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux split. -### bash +```sh +# usage: fzf-tmux [-h HEIGHT[%]] [-w WIDTH[%]] [--] [FZF OPTIONS] + +# select git branches in horizontal split (15 lines) +git branch | fzf-tmux -h 15 + +# select multiple words in vertical split (20% of screen width) +cat /usr/share/dict/words | fzf-tmux -w 20% --multi +``` + +It will still work even when you're not on tmux, silently ignoring `-h` and +`-w` options, so you can invariably use `fzf-tmux` in your scripts. + +Fuzzy completion for bash +------------------------- #### Files and directories @@ -300,18 +234,12 @@ export FZF_COMPLETION_TRIGGER='~~' export FZF_COMPLETION_OPTS='+c -x' ``` -### zsh - -TODO :smiley: - -(Pull requests are appreciated.) - Usage as Vim plugin ------------------- (Note: To use fzf in GVim, an external terminal emulator is required.) -### `:FZF[!]` +#### `:FZF[!]` If you have set up fzf for Vim, `:FZF` command will be added. @@ -349,7 +277,7 @@ Refer to the [this wiki page](https://github.com/junegunn/fzf/wiki/fzf-with-MacVim-and-iTerm2) to see how to set up. -### `fzf#run([options])` +#### `fzf#run([options])` For more advanced uses, you can call `fzf#run()` function which returns the list of the selected items. @@ -368,7 +296,7 @@ of the selected items. | `tmux_height` | number/string | Use tmux horizontal split with the given height (e.g. `20`, `50%`) | | `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) | -#### Examples +##### Examples If `sink` option is not given, `fzf#run` will simply return the list. @@ -423,14 +351,17 @@ nnoremap :call fzf#run({ \ }) ``` -### Articles +More examples can be found on [the wiki +page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). + +#### Articles - [fzf+vim+tmux](http://junegunn.kr/2014/04/fzf+vim+tmux) Tips ---- -### Rendering issues +#### Rendering issues If you have any rendering issues, check the followings: @@ -444,7 +375,7 @@ If you have any rendering issues, check the followings: `FZF_DEFAULT_OPTS` for further convenience. 4. If you still have problem, try `--no-256` option or even `--no-color`. -### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` +#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` [ag](https://github.com/ggreer/the_silver_searcher) or [pt](https://github.com/monochromegane/the_platinum_searcher) will do the @@ -461,7 +392,7 @@ export FZF_DEFAULT_COMMAND='ag -l -g ""' fzf ``` -### `git ls-tree` for fast traversal +#### `git ls-tree` for fast traversal If you're running fzf in a large git repository, `git ls-tree` can boost up the speed of the traversal. @@ -481,10 +412,12 @@ fzf() { } ``` -### Using fzf with tmux splits +#### Using fzf with tmux splits -It isn't too hard to write your own fzf-tmux combo like the default -CTRL-T key binding. (Or is it?) +The supplied [fzf-tmux](bin/fzf-tmux) script should suffice in most of the +cases, but if you want to be able to update command line like the default +`CTRL-T` key binding, you'll have to use `send-keys` command of tmux. The +following example will show you how it can be done. ```sh # This is a helper function that splits the current pane to start the given @@ -514,7 +447,7 @@ fzf_tmux_dir() { bind '"\C-x\C-d": "$(fzf_tmux_dir)\e\C-e"' ``` -### Fish shell +#### Fish shell It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362) that it doesn't allow reading from STDIN in command substitution, which means @@ -537,7 +470,7 @@ function fe end ``` -### Handling UTF-8 NFD paths on OSX +#### Handling UTF-8 NFD paths on OSX Use iconv to convert NFD paths to NFC: From c111af0ed23731942ac8cbeb88c9f9e5d819628a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 7 Mar 2015 09:08:41 +0900 Subject: [PATCH 106/408] Use the term `pane` instead of `split` when not ambiguous /cc @Tranquility --- README.md | 6 +++--- bin/fzf-tmux | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c7b79d3..230804f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Installation fzf project consists of the followings: - `fzf` executable -- `fzf-tmux` script for launching fzf in a tmux split +- `fzf-tmux` script for launching fzf in a tmux pane - Shell extensions - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) - Fuzzy auto-completion (bash only) @@ -149,7 +149,7 @@ installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and `fzf-tmux` script ----------------- -[fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux split. +[fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane. ```sh # usage: fzf-tmux [-h HEIGHT[%]] [-w WIDTH[%]] [--] [FZF OPTIONS] @@ -412,7 +412,7 @@ fzf() { } ``` -#### Using fzf with tmux splits +#### Using fzf with tmux panes The supplied [fzf-tmux](bin/fzf-tmux) script should suffice in most of the cases, but if you want to be able to update command line like the default diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 02d935e..4db037c 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# fzf-tmux: starts fzf in a tmux split +# fzf-tmux: starts fzf in a tmux pane # usage: fzf-tmux [-h HEIGHT[%]] [-w WIDTH[%]] [--] [FZF OPTIONS] args=() From f7b52d2541fba6f10a86d33399f38fc38efcbc2a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 7 Mar 2015 09:29:16 +0900 Subject: [PATCH 107/408] Use absolute path of fzf when splitting tmux window --- bin/fzf-tmux | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 4db037c..d945c15 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -50,13 +50,19 @@ cleanup() { } trap cleanup EXIT SIGINT SIGTERM +fail() { + >&2 echo "$1" + exit 1 +} +fzf=$(which fzf 2> /dev/null) || fail "fzf executable not found" + mkfifo $fifo2 mkfifo $fifo3 if [ -t 0 ]; then - tmux split-window $opt 'bash -c "fzf '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3'"' + tmux split-window $opt 'sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3'"' else mkfifo $fifo1 - tmux split-window $opt 'bash -c "fzf '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3'"' + tmux split-window $opt 'sh -c "'$fzf' '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3'"' cat <&0 > $fifo1 & fi cat $fifo2 From 06ab3994979278e33d4e7c6f02ae90055981eca8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 7 Mar 2015 09:46:32 +0900 Subject: [PATCH 108/408] Improve how vim plugin finds fzf executable This avoids the problem in which :FZF command silently fails when fzf executable cannot be found in $PATH of the hosting tmux server. --- plugin/fzf.vim | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 22fb4cc..ba49c7e 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -33,17 +33,24 @@ set cpo&vim function! s:fzf_exec() if !exists('s:exec') - call system('type fzf') - if v:shell_error - let s:exec = executable(s:fzf_go) ? - \ s:fzf_go : (executable(s:fzf_rb) ? s:fzf_rb : '') + if executable(s:fzf_go) + let s:exec = s:fzf_go else - let s:exec = 'fzf' + let path = split(system('which fzf 2> /dev/null'), '\n') + if !v:shell_error && !empty(path) + let s:exec = path[0] + elseif executable(s:fzf_rb) + let s:exec = s:fzf_rb + else + call system('type fzf') + if v:shell_error + throw 'fzf executable not found' + else + let s:exec = 'fzf' + endif + endif endif - return s:fzf_exec() - elseif empty(s:exec) - unlet s:exec - throw 'fzf executable not found' + return s:exec else return s:exec endif From 87c71a3ea6fbc5dd343e92a166cb9478891274a9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 7 Mar 2015 09:53:54 +0900 Subject: [PATCH 109/408] Increase timeout in test cases --- test/test_go.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 524e436..299fefa 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -138,7 +138,7 @@ class Tmux end private def defaults opts - { timeout: 5, pane: 0 }.merge(opts) + { timeout: 10, pane: 0 }.merge(opts) end def wait opts = {} @@ -209,7 +209,7 @@ class TestGoFZF < TestBase def test_vanilla tmux.send_keys "seq 1 100000 | #{fzf}", :Enter - tmux.until(timeout: 10) { |lines| + tmux.until(timeout: 20) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } lines = tmux.capture assert_equal ' 2', lines[-4] From 5f62d224b0cef3e39667d218cd74be34b2c8219c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 7 Mar 2015 10:07:36 +0900 Subject: [PATCH 110/408] Fix fzf-tmux script (bash 3.2 compatibility) --- bin/fzf-tmux | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index d945c15..b4e041d 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -15,7 +15,7 @@ while [ $# -gt 0 ]; do size="$1" fi [[ "$arg" =~ ^-w ]] && opt="-h" - [[ "$size" =~ %$ ]] && opt="$opt -p ${size:0:-1}" || + [[ "$size" =~ %$ ]] && opt="$opt -p ${size:0:((${#size}-1))}" || opt="$opt -l $size" ;; --) From 23dba99eda58ddae147a2e907513fd0471c2014d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Mar 2015 15:08:27 +0900 Subject: [PATCH 111/408] [fzf-tmux] Allow -w / -h without size argument --- README.md | 2 +- bin/fzf-tmux | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 230804f..2ef3b9e 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and [fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane. ```sh -# usage: fzf-tmux [-h HEIGHT[%]] [-w WIDTH[%]] [--] [FZF OPTIONS] +# usage: fzf-tmux [-h [HEIGHT[%]]] [-w [WIDTH[%]]] [--] [FZF OPTIONS] # select git branches in horizontal split (15 lines) git branch | fzf-tmux -h 15 diff --git a/bin/fzf-tmux b/bin/fzf-tmux index b4e041d..166c811 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -1,6 +1,6 @@ #!/usr/bin/env bash # fzf-tmux: starts fzf in a tmux pane -# usage: fzf-tmux [-h HEIGHT[%]] [-w WIDTH[%]] [--] [FZF OPTIONS] +# usage: fzf-tmux [-h [HEIGHT[%]]] [-w [WIDTH[%]]] [--] [FZF OPTIONS] args=() opt="" @@ -8,15 +8,20 @@ while [ $# -gt 0 ]; do arg="$1" case "$arg" in -w*|-h*) + [[ "$arg" =~ ^-w ]] && opt="-h" || opt="" if [ ${#arg} -gt 2 ]; then size="${arg:2}" else shift - size="$1" + if [[ "$1" =~ ^[0-9]+%?$ ]]; then + size="$1" + else + [ -n "$1" ] && args+=("$1") + continue + fi fi - [[ "$arg" =~ ^-w ]] && opt="-h" - [[ "$size" =~ %$ ]] && opt="$opt -p ${size:0:((${#size}-1))}" || - opt="$opt -l $size" + [[ "$size" =~ %$ ]] && opt="$opt -p ${size:0:((${#size}-1))}" || + opt="$opt -l $size" ;; --) # "--" can be used to separate fzf-tmux options from fzf options to From 62a28468a75d5136dd09560716829b8164d9a985 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Mar 2015 16:36:37 +0900 Subject: [PATCH 112/408] [fzf-tmux] Fix `--` --- bin/fzf-tmux | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 166c811..f7470e0 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -4,10 +4,16 @@ args=() opt="" +skip="" while [ $# -gt 0 ]; do arg="$1" case "$arg" in -w*|-h*) + if [ -n "$skip" ]; then + args+=("$1") + shift + continue + fi [[ "$arg" =~ ^-w ]] && opt="-h" || opt="" if [ ${#arg} -gt 2 ]; then size="${arg:2}" @@ -26,7 +32,7 @@ while [ $# -gt 0 ]; do --) # "--" can be used to separate fzf-tmux options from fzf options to # avoid conflicts - break + skip=1 ;; *) args+=("$1") From fb2959c514e433e4cc6a18c4b368cc9cb6158507 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Mar 2015 16:39:30 +0900 Subject: [PATCH 113/408] [fzf-tmux] Fix duplicate arguments to fzf fzf-tmux -w -q q fzf-tmux -w -- -q q --- bin/fzf-tmux | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index f7470e0..1e045e7 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -22,7 +22,8 @@ while [ $# -gt 0 ]; do if [[ "$1" =~ ^[0-9]+%?$ ]]; then size="$1" else - [ -n "$1" ] && args+=("$1") + [ -n "$1" -a "$1" != "--" ] && args+=("$1") + shift continue fi fi From 789a474b28ca157860daa90a7f9a76e4b783a6c2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Mar 2015 02:50:43 +0900 Subject: [PATCH 114/408] [fzf-tmux] Allow opening fzf on any position (-u/-d/-l/-r) The previous -w and -h will be synonyms for -r and -d respectively. --- README.md | 14 +++++++------- bin/fzf-tmux | 48 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2ef3b9e..9f61d3f 100644 --- a/README.md +++ b/README.md @@ -152,17 +152,17 @@ installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and [fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane. ```sh -# usage: fzf-tmux [-h [HEIGHT[%]]] [-w [WIDTH[%]]] [--] [FZF OPTIONS] +# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] -# select git branches in horizontal split (15 lines) -git branch | fzf-tmux -h 15 +# select git branches in horizontal split below (15 lines) +git branch | fzf-tmux -d 15 -# select multiple words in vertical split (20% of screen width) -cat /usr/share/dict/words | fzf-tmux -w 20% --multi +# select multiple words in vertical split on the left (20% of screen width) +cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse ``` -It will still work even when you're not on tmux, silently ignoring `-h` and -`-w` options, so you can invariably use `fzf-tmux` in your scripts. +It will still work even when you're not on tmux, silently ignoring `-[udlr]` +options, so you can invariably use `fzf-tmux` in your scripts. Fuzzy completion for bash ------------------------- diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 1e045e7..6b833db 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -1,20 +1,36 @@ #!/usr/bin/env bash # fzf-tmux: starts fzf in a tmux pane -# usage: fzf-tmux [-h [HEIGHT[%]]] [-w [WIDTH[%]]] [--] [FZF OPTIONS] +# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] args=() opt="" skip="" +swap="" +close="" while [ $# -gt 0 ]; do arg="$1" case "$arg" in - -w*|-h*) + -w*|-h*|-d*|-u*|-r*|-l*) if [ -n "$skip" ]; then args+=("$1") shift continue fi - [[ "$arg" =~ ^-w ]] && opt="-h" || opt="" + if [[ "$arg" =~ ^.[lrw] ]]; then + opt="-h" + if [[ "$arg" =~ ^.l ]]; then + opt="$opt -d" + swap="; swap-pane -D ; select-pane -L" + close="; tmux swap-pane -D" + fi + else + opt="" + if [[ "$arg" =~ ^.u ]]; then + opt="$opt -d" + swap="; swap-pane -D ; select-pane -U" + close="; tmux swap-pane -D" + fi + fi if [ ${#arg} -gt 2 ]; then size="${arg:2}" else @@ -27,8 +43,24 @@ while [ $# -gt 0 ]; do continue fi fi - [[ "$size" =~ %$ ]] && opt="$opt -p ${size:0:((${#size}-1))}" || - opt="$opt -l $size" + + if [[ "$size" =~ %$ ]]; then + size=${size:0:((${#size}-1))} + if [ -n "$swap" ]; then + opt="$opt -p $(( 100 - size ))" + else + opt="$opt -p $size" + fi + else + if [ -n "$swap" ]; then + [[ "$arg" =~ ^.l ]] && max=$(tput cols) || max=$(tput lines) + size=$(( max - size )) + [ $size -lt 0 ] && size=0 + opt="$opt -l $size" + else + opt="$opt -l $size" + fi + fi ;; --) # "--" can be used to separate fzf-tmux options from fzf options to @@ -71,10 +103,12 @@ fzf=$(which fzf 2> /dev/null) || fail "fzf executable not found" mkfifo $fifo2 mkfifo $fifo3 if [ -t 0 ]; then - tmux split-window $opt 'sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3'"' + tmux set-window-option -q synchronize-panes off \;\ + split-window $opt 'sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap else mkfifo $fifo1 - tmux split-window $opt 'sh -c "'$fzf' '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3'"' + tmux set-window-option -q synchronize-panes off \;\ + split-window $opt 'sh -c "'$fzf' '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap cat <&0 > $fifo1 & fi cat $fifo2 From 2b8a1c0d7032bc6256055c4f97bec622831888a4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Mar 2015 23:40:43 +0900 Subject: [PATCH 115/408] Update README - Homebrew instruction and fzf-tmux options --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f61d3f..58a3827 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. brew install fzf # Install shell extensions - this should be done whenever fzf is updated -/usr/local/Cellar/fzf/$(fzf --version)/install +$(brew info fzf | grep /install) ``` #### Install as Vim plugin @@ -153,6 +153,7 @@ installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and ```sh # usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] +# (-[udlr]: up/down/left/right) # select git branches in horizontal split below (15 lines) git branch | fzf-tmux -d 15 From c22e729d9c8810f8c227b353367c3b60ae333ee0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Mar 2015 23:57:17 +0900 Subject: [PATCH 116/408] [fzf-tmux] Apply environment variables --- bin/fzf-tmux | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 6b833db..0eb2e37 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -99,16 +99,17 @@ fail() { exit 1 } fzf=$(which fzf 2> /dev/null) || fail "fzf executable not found" +envs="FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" mkfifo $fifo2 mkfifo $fifo3 if [ -t 0 ]; then tmux set-window-option -q synchronize-panes off \;\ - split-window $opt 'sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "$envs"' sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap else mkfifo $fifo1 tmux set-window-option -q synchronize-panes off \;\ - split-window $opt 'sh -c "'$fzf' '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "$envs"' sh -c "'$fzf' '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap cat <&0 > $fifo1 & fi cat $fifo2 From 12d81e212f3bd933532e65ed0de9f06c7746f222 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Mar 2015 01:41:35 +0900 Subject: [PATCH 117/408] [vim] Use fzf-tmux script for tmux integration --- README.md | 37 +++++++++--------- bin/fzf-tmux | 12 +++++- plugin/fzf.vim | 101 ++++++++++++++++++++++--------------------------- 3 files changed, 74 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 58a3827..22fce29 100644 --- a/README.md +++ b/README.md @@ -285,17 +285,16 @@ of the selected items. `fzf#run()` may take an options-dictionary: -| Option name | Type | Description | -| --------------- | ------------- | ------------------------------------------------------------------ | -| `source` | string | External command to generate input to fzf (e.g. `find .`) | -| `source` | list | Vim list as input to fzf | -| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) | -| `sink` | funcref | Reference to function to process each selected item | -| `options` | string | Options to fzf | -| `dir` | string | Working directory | -| `tmux_width` | number/string | Use tmux vertical split with the given height (e.g. `20`, `50%`) | -| `tmux_height` | number/string | Use tmux horizontal split with the given height (e.g. `20`, `50%`) | -| `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) | +| Option name | Type | Description | +| -------------------------- | ------------- | ---------------------------------------------------------------- | +| `source` | string | External command to generate input to fzf (e.g. `find .`) | +| `source` | list | Vim list as input to fzf | +| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) | +| `sink` | funcref | Reference to function to process each selected item | +| `options` | string | Options to fzf | +| `dir` | string | Working directory | +| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | +| `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) | ##### Examples @@ -321,10 +320,10 @@ nnoremap C :call fzf#run({ \ 'source': \ map(split(globpath(&rtp, "colors/*.vim"), "\n"), \ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"), -\ 'sink': 'colo', -\ 'options': '+m', -\ 'tmux_width': 20, -\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s' +\ 'sink': 'colo', +\ 'options': '+m', +\ 'left': 20, +\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s' \ }) ``` @@ -345,10 +344,10 @@ function! BufOpen(e) endfunction nnoremap :call fzf#run({ -\ 'source': reverse(BufList()), -\ 'sink': function('BufOpen'), -\ 'options': '+m', -\ 'tmux_height': '40%' +\ 'source': reverse(BufList()), +\ 'sink': function('BufOpen'), +\ 'options': '+m', +\ 'down': '40%' \ }) ``` diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 0eb2e37..324562a 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -7,9 +7,13 @@ opt="" skip="" swap="" close="" +term="" while [ $# -gt 0 ]; do arg="$1" case "$arg" in + -) + term=1 + ;; -w*|-h*|-d*|-u*|-r*|-l*) if [ -n "$skip" ]; then args+=("$1") @@ -53,7 +57,11 @@ while [ $# -gt 0 ]; do fi else if [ -n "$swap" ]; then - [[ "$arg" =~ ^.l ]] && max=$(tput cols) || max=$(tput lines) + if [[ "$arg" =~ ^.l ]]; then + [ -n "$COLUMNS" ] && max=$COLUMNS || max=$(tput cols) + else + [ -n "$LINES" ] && max=$LINES || max=$(tput lines) + fi size=$(( max - size )) [ $size -lt 0 ] && size=0 opt="$opt -l $size" @@ -103,7 +111,7 @@ envs="FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") FZF_DEFAULT_COMMAND=$(pr mkfifo $fifo2 mkfifo $fifo3 -if [ -t 0 ]; then +if [ -n "$term" -o -t 0 ]; then tmux set-window-option -q synchronize-panes off \;\ split-window $opt "$envs"' sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap else diff --git a/plugin/fzf.vim b/plugin/fzf.vim index ba49c7e..69e7f71 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -21,12 +21,11 @@ " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -let s:min_tmux_width = 10 -let s:min_tmux_height = 3 let s:default_tmux_height = '40%' let s:launcher = 'xterm -e bash -ic %s' let s:fzf_go = expand(':h:h').'/bin/fzf' let s:fzf_rb = expand(':h:h').'/fzf' +let s:fzf_tmux = expand(':h:h').'/bin/fzf-tmux' let s:cpo_save = &cpo set cpo&vim @@ -66,7 +65,7 @@ function! s:tmux_enabled() endif let s:tmux = 0 - if exists('$TMUX') + if exists('$TMUX') && executable(s:fzf_tmux) let output = system('tmux -V') let s:tmux = !v:shell_error && output >= 'tmux 1.7' endif @@ -81,8 +80,23 @@ function! s:escape(path) return substitute(a:path, ' ', '\\ ', 'g') endfunction +" Upgrade legacy options +function! s:upgrade(dict) + let copy = copy(a:dict) + if has_key(copy, 'tmux') + let copy.down = remove(copy, 'tmux') + endif + if has_key(copy, 'tmux_height') + let copy.down = remove(copy, 'tmux_height') + endif + if has_key(copy, 'tmux_width') + let copy.right = remove(copy, 'tmux_width') + endif + return copy +endfunction + function! fzf#run(...) abort - let dict = exists('a:1') ? a:1 : {} + let dict = exists('a:1') ? s:upgrade(a:1) : {} let temps = { 'result': tempname() } let optstr = get(dict, 'options', '') try @@ -106,26 +120,41 @@ function! fzf#run(...) abort else let prefix = '' endif - let command = prefix.fzf_exec.' '.optstr.' > '.temps.result + let split = s:tmux_enabled() && s:tmux_splittable(dict) + let command = prefix.(split ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result - if s:tmux_enabled() && s:tmux_splittable(dict) + if split return s:execute_tmux(dict, command, temps) else return s:execute(dict, command, temps) endif endfunction +function! s:fzf_tmux(dict) + let size = '' + for o in ['up', 'down', 'left', 'right'] + if has_key(a:dict, o) + let size = '-'.o[0].a:dict[o] + endif + endfor + return printf('LINES=%d COLUMNS=%d %s %s %s --', + \ &lines, &columns, s:fzf_tmux, size, (has_key(a:dict, 'source') ? '' : '-')) +endfunction + function! s:tmux_splittable(dict) - return - \ min([&columns, get(a:dict, 'tmux_width', 0)]) >= s:min_tmux_width || - \ min([&lines, get(a:dict, 'tmux_height', get(a:dict, 'tmux', 0))]) >= s:min_tmux_height + return has_key(a:dict, 'up') || + \ has_key(a:dict, 'down') || + \ has_key(a:dict, 'left') || + \ has_key(a:dict, 'right') endfunction function! s:pushd(dict) if !empty(get(a:dict, 'dir', '')) let a:dict.prev_dir = getcwd() execute 'chdir '.s:escape(a:dict.dir) + return 1 endif + return 0 endfunction function! s:popd(dict) @@ -153,7 +182,7 @@ function! s:execute(dict, command, temps) endif return [] else - return s:callback(a:dict, a:temps, 0) + return s:callback(a:dict, a:temps) endif endfunction @@ -166,58 +195,20 @@ function! s:env_var(name) endfunction function! s:execute_tmux(dict, command, temps) - let command = s:env_var('FZF_DEFAULT_OPTS').s:env_var('FZF_DEFAULT_COMMAND').a:command - if !empty(get(a:dict, 'dir', '')) + let command = a:command + if s:pushd(a:dict) + " -c '#{pane_current_path}' is only available on tmux 1.9 or above let command = 'cd '.s:escape(a:dict.dir).' && '.command endif - let splitopt = '-v' - if has_key(a:dict, 'tmux_width') - let splitopt = '-h' - let size = a:dict.tmux_width - else - let size = get(a:dict, 'tmux_height', get(a:dict, 'tmux')) - endif - - if type(size) == 1 && size =~ '%$' - let sizeopt = '-p '.size[0:-2] - else - let sizeopt = '-l '.size - endif - - let s:pane = substitute( - \ system( - \ printf( - \ 'tmux split-window %s %s -P -F "#{pane_id}" %s', - \ splitopt, sizeopt, s:shellesc(command))), '\n', '', 'g') - let s:dict = a:dict - let s:temps = a:temps - - augroup fzf_tmux - autocmd! - autocmd VimResized * nested call s:tmux_check() - augroup END + call system(command) + call s:callback(a:dict, a:temps) endfunction -function! s:tmux_check() - let panes = split(system('tmux list-panes -a -F "#{pane_id}"'), '\n') - - if index(panes, s:pane) < 0 - augroup fzf_tmux - autocmd! - augroup END - - call s:callback(s:dict, s:temps, 1) - redraw - endif -endfunction - -function! s:callback(dict, temps, cd) +function! s:callback(dict, temps) if !filereadable(a:temps.result) let lines = [] else - if a:cd | call s:pushd(a:dict) | endif - let lines = readfile(a:temps.result) if has_key(a:dict, 'sink') for line in lines @@ -246,7 +237,7 @@ function! s:cmd(bang, ...) abort let opts.dir = remove(args, -1) endif if !a:bang - let opts.tmux = get(g:, 'fzf_tmux_height', s:default_tmux_height) + let opts.down = get(g:, 'fzf_tmux_height', s:default_tmux_height) endif call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts)) endfunction From 48e0c1e721a5a18ad4066a81170995db96a7610a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Mar 2015 02:16:32 +0900 Subject: [PATCH 118/408] Ignore new options in legacy Ruby version --- fzf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fzf b/fzf index c60cc9f..eb36391 100755 --- a/fzf +++ b/fzf @@ -200,6 +200,8 @@ class FZF when '--no-print-query' then @print_query = false when '-e', '--extended-exact' then @extended = :exact when '+e', '--no-extended-exact' then @extended = nil + when '--tac', '--sync' + # XXX else usage 1, "illegal option: #{o}" end From daa79a6df2282e7d0b45e648dcca3ed077a58828 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Mar 2015 12:07:32 +0900 Subject: [PATCH 119/408] [vim] fzf#run with tmux panes can now return values to the caller As they're made synchronous with the use of fzf-tmux script --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 69e7f71..a3b6abd 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -202,7 +202,7 @@ function! s:execute_tmux(dict, command, temps) endif call system(command) - call s:callback(a:dict, a:temps) + return s:callback(a:dict, a:temps) endfunction function! s:callback(dict, temps) From 49c752b1f70a72000662f798bcff7ccf23b96e02 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Mar 2015 12:13:11 +0900 Subject: [PATCH 120/408] [vim] up/down/left/right options to take boolean values When 1 is given, 50% of the screen width or height will be used as the default size of the pane. --- plugin/fzf.vim | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index a3b6abd..076ab91 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -130,11 +130,20 @@ function! fzf#run(...) abort endif endfunction +function! s:present(dict, ...) + for key in a:000 + if !empty(get(a:dict, key, '')) + return 1 + endif + endfor + return 0 +endfunction + function! s:fzf_tmux(dict) let size = '' for o in ['up', 'down', 'left', 'right'] - if has_key(a:dict, o) - let size = '-'.o[0].a:dict[o] + if s:present(a:dict, o) + let size = '-'.o[0].(a:dict[o] == 1 ? '' : a:dict[o]) endif endfor return printf('LINES=%d COLUMNS=%d %s %s %s --', @@ -142,14 +151,11 @@ function! s:fzf_tmux(dict) endfunction function! s:tmux_splittable(dict) - return has_key(a:dict, 'up') || - \ has_key(a:dict, 'down') || - \ has_key(a:dict, 'left') || - \ has_key(a:dict, 'right') + return s:present(a:dict, 'up', 'down', 'left', 'right') endfunction function! s:pushd(dict) - if !empty(get(a:dict, 'dir', '')) + if s:present(a:dict, 'dir') let a:dict.prev_dir = getcwd() execute 'chdir '.s:escape(a:dict.dir) return 1 From b2c423d1ffad3ca51c63efbe94467cde7c905f7e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Mar 2015 02:12:38 +0900 Subject: [PATCH 121/408] Cleanup - no more rubygems --- .travis.yml | 2 +- Rakefile | 12 ------------ ext/mkrf_conf.rb | 9 --------- fzf.gemspec | 17 ----------------- 4 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 Rakefile delete mode 100644 ext/mkrf_conf.rb delete mode 100644 fzf.gemspec diff --git a/.travis.yml b/.travis.yml index 6908677..cce98ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,4 +25,4 @@ script: | cd $FZF_BASE/src && make test fzf/fzf-linux_amd64 install && cd $FZF_BASE/bin && ln -sf fzf-linux_amd64 fzf-$(./fzf --version)-linux_amd64 && cd $FZF_BASE && yes | ./install && - tmux new "rake test > out && touch ok" && cat out && [ -e ok ] + tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ] diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 5bc903d..0000000 --- a/Rakefile +++ /dev/null @@ -1,12 +0,0 @@ -require "bundler/gem_tasks" -require 'rake/testtask' - -Rake::TestTask.new(:test) do |test| - test.pattern = 'test/test_go.rb' -end - -Rake::TestTask.new(:testall) do |test| - test.pattern = 'test/test_*.rb' -end - -task :default => :test diff --git a/ext/mkrf_conf.rb b/ext/mkrf_conf.rb deleted file mode 100644 index 8e7bf46..0000000 --- a/ext/mkrf_conf.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'rubygems/dependency_installer' - -if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.1.0') - Gem::DependencyInstaller.new.install 'curses', '~> 1.0' -end - -File.open(File.expand_path('../Rakefile', __FILE__), 'w') do |f| - f.puts 'task :default' -end diff --git a/fzf.gemspec b/fzf.gemspec deleted file mode 100644 index 0a6c74d..0000000 --- a/fzf.gemspec +++ /dev/null @@ -1,17 +0,0 @@ -# coding: utf-8 -Gem::Specification.new do |spec| - spec.name = 'fzf' - spec.version = '0.8.4' - spec.authors = ['Junegunn Choi'] - spec.email = ['junegunn.c@gmail.com'] - spec.description = %q{Fuzzy finder for your shell} - spec.summary = %q{Fuzzy finder for your shell} - spec.homepage = 'https://github.com/junegunn/fzf' - spec.license = 'MIT' - - spec.bindir = '.' - spec.files = %w[fzf.gemspec] - spec.executables = 'fzf' - - spec.extensions += ['ext/mkrf_conf.rb'] -end From c4d59aeec44a0d94e29788a7a6ebd036d6b48409 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Mar 2015 02:16:27 +0900 Subject: [PATCH 122/408] Remove legacy test code --- test/test_ruby.rb | 850 ---------------------------------------------- 1 file changed, 850 deletions(-) delete mode 100644 test/test_ruby.rb diff --git a/test/test_ruby.rb b/test/test_ruby.rb deleted file mode 100644 index 25f923b..0000000 --- a/test/test_ruby.rb +++ /dev/null @@ -1,850 +0,0 @@ -#!/usr/bin/env ruby -# encoding: utf-8 - -require 'rubygems' -require 'curses' -require 'timeout' -require 'stringio' -require 'minitest/autorun' -require 'tempfile' -$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 - self - end -end - -class TestRubyFZF < Minitest::Test - def setup - ENV.delete 'FZF_DEFAULT_SORT' - ENV.delete 'FZF_DEFAULT_OPTS' - ENV.delete 'FZF_DEFAULT_COMMAND' - end - - def test_default_options - fzf = FZF.new [] - assert_equal 1000, fzf.sort - assert_equal false, fzf.multi - assert_equal true, fzf.color - 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 - assert_equal '', fzf.query - assert_equal false, fzf.select1 - assert_equal false, fzf.exit0 - assert_equal nil, fzf.filter - assert_equal nil, fzf.extended - assert_equal false, fzf.reverse - assert_equal '> ', fzf.prompt - assert_equal false, fzf.print_query - end - - def test_environment_variables - # Deprecated - 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 --select-1 -0 ' << - '--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 ', - fzf.query - assert_equal 'goodbye world', - fzf.filter - assert_equal :fuzzy, fzf.extended - assert_equal true, fzf.multi - assert_equal false, fzf.color - 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 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 --with-nth=.. --reverse --prompt (hi) - --print-query] - assert_equal 2000, fzf.sort - assert_equal true, fzf.multi - assert_equal false, fzf.color - assert_equal false, fzf.ansi256 - assert_equal false, fzf.black - assert_equal false, fzf.mouse - assert_equal 0, fzf.rxflag - assert_equal 'hello', fzf.query - assert_equal true, fzf.select1 - assert_equal true, fzf.exit0 - 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 - - # 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 - --reverse --no-reverse --prompt (hi) --prompt=(HI) - --print-query --no-print-query] - assert_equal nil, fzf.sort - assert_equal false, fzf.multi - assert_equal true, fzf.color - assert_equal true, fzf.ansi256 - assert_equal true, fzf.black - assert_equal true, fzf.mouse - assert_equal 1, fzf.rxflag - assert_equal 'b', fzf.filter - assert_equal 'hello', fzf.query - assert_equal false, fzf.select1 - assert_equal false, fzf.exit0 - assert_equal nil, fzf.extended - assert_equal [-2..-2], fzf.nth - assert_equal false, fzf.reverse - assert_equal '(HI)', fzf.prompt - assert_equal false, fzf.print_query - - # Short opts - 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 - assert_equal false, fzf.ansi256 - assert_equal 0, fzf.rxflag - assert_equal 'hello', fzf.query - assert_equal 'howdy', fzf.filter - assert_equal :fuzzy, fzf.extended - assert_equal [2..2], 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 - -1 -0 +1 +0 - ] - assert_equal 3000, fzf.sort - assert_equal false, fzf.multi - assert_equal true, fzf.color - assert_equal true, fzf.ansi256 - assert_equal false, fzf.black - assert_equal 1, fzf.rxflag - assert_equal 'world', fzf.query - assert_equal false, fzf.select1 - assert_equal false, fzf.exit0 - assert_equal 'world', fzf.filter - assert_equal nil, fzf.extended - assert_equal [3..3, 4..4], fzf.nth - rescue SystemExit => e - assert false, "Exited" - end - - def test_invalid_option - [ - %w[--unknown], - %w[yo dawg], - %w[--nth=0], - %w[-n 0], - %w[-n 1..2..3], - %w[-n 1....], - %w[-n ....3], - %w[-n 1....3], - %w[-n 1..0], - %w[--nth ..0], - ].each do |argv| - assert_raises(SystemExit) do - fzf = FZF.new argv - end - end - end - - def test_width - fzf = FZF.new [] - assert_equal 5, fzf.width('abcde') - assert_equal 4, fzf.width('한글') - assert_equal 5, fzf.width('한글.') - end if RUBY_VERSION >= '1.9' - - def test_trim - fzf = FZF.new [] - assert_equal ['사.', 6], fzf.trim('가나다라마바사.', 4, true) - assert_equal ['바사.', 5], fzf.trim('가나다라마바사.', 5, true) - assert_equal ['바사.', 5], fzf.trim('가나다라마바사.', 6, true) - assert_equal ['마바사.', 4], fzf.trim('가나다라마바사.', 7, true) - assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 4, false) - assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 5, false) - 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 if RUBY_VERSION >= '1.9' - - def test_format - fzf = FZF.new [] - assert_equal [['01234..', false]], fzf.format('0123456789', 7, []) - assert_equal [['012', false], ['34', true], ['..', false]], - fzf.format('0123456789', 7, [[3, 5]]) - assert_equal [['..56', false], ['789', true]], - fzf.format('0123456789', 7, [[7, 10]]) - assert_equal [['..56', false], ['78', true], ['9', false]], - fzf.format('0123456789', 7, [[7, 9]]) - - (3..5).each do |i| - assert_equal [['..', false], ['567', true], ['89', false]], - fzf.format('0123456789', 7, [[i, 8]]) - end - - assert_equal [['..', false], ['345', true], ['..', false]], - fzf.format('0123456789', 7, [[3, 6]]) - assert_equal [['012', false], ['34', true], ['..', false]], - fzf.format('0123456789', 7, [[3, 5]]) - - # Multi-region - assert_equal [["0", true], ["1", false], ["2", true], ["34..", false]], - fzf.format('0123456789', 7, [[0, 1], [2, 3]]) - - assert_equal [["..", false], ["5", true], ["6", false], ["78", true], ["9", false]], - fzf.format('0123456789', 7, [[3, 6], [7, 9]]) - - assert_equal [["..", false], ["3", true], ["4", false], ["5", true], ["..", false]], - fzf.format('0123456789', 7, [[3, 4], [5, 6]]) - - # Multi-region Overlap - assert_equal [["..", false], ["345", true], ["..", false]], - fzf.format('0123456789', 7, [[4, 5], [3, 6]]) - end - - def test_fuzzy_matcher - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE - list = %w[ - juice - juiceful - juiceless - juicily - juiciness - juicy] - assert matcher.caches.empty? - assert_equal( - [["juice", [[0, 1]]], - ["juiceful", [[0, 1]]], - ["juiceless", [[0, 1]]], - ["juicily", [[0, 1]]], - ["juiciness", [[0, 1]]], - ["juicy", [[0, 1]]]], matcher.match(list, 'j', '', '').sort) - assert !matcher.caches.empty? - assert_equal [list.object_id], matcher.caches.keys - assert_equal 1, matcher.caches[list.object_id].length - assert_equal 6, matcher.caches[list.object_id]['j'].length - - assert_equal( - [["juicily", [[0, 5]]], - ["juiciness", [[0, 5]]]], matcher.match(list, 'jii', '', '').sort) - - assert_equal( - [["juicily", [[2, 5]]], - ["juiciness", [[2, 5]]]], matcher.match(list, 'ii', '', '').sort) - - assert_equal 3, matcher.caches[list.object_id].length - assert_equal 2, matcher.caches[list.object_id]['ii'].length - - # TODO : partial_cache - end - - def test_fuzzy_matcher_rxflag - assert_equal nil, FZF::FuzzyMatcher.new(nil).rxflag - assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag - assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag - - assert_equal 1, FZF::FuzzyMatcher.new(nil).rxflag_for('abc') - assert_equal 0, FZF::FuzzyMatcher.new(nil).rxflag_for('Abc') - assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag_for('abc') - assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag_for('Abc') - assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag_for('abc') - assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag_for('Abc') - end - - def test_fuzzy_matcher_case_sensitive - # Smart-case match (Uppercase found) - assert_equal [['Fruit', [[0, 5]]]], - FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort - - # Smart-case match (Uppercase not-found) - assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]], - FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'fruit', '', '').sort - - # Case-sensitive match (-i) - assert_equal [['Fruit', [[0, 5]]]], - FZF::FuzzyMatcher.new(0).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort - - # Case-insensitive match (+i) - assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]], - FZF::FuzzyMatcher.new(Regexp::IGNORECASE). - match(%w[Fruit Grapefruit], 'Fruit', '', '').sort - end - - def test_extended_fuzzy_matcher_case_sensitive - %w['Fruit Fruit$].each do |q| - # Smart-case match (Uppercase found) - assert_equal [['Fruit', [[0, 5]]]], - FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q, '', '').sort - - # Smart-case match (Uppercase not-found) - assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]], - FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q.downcase, '', '').sort - - # Case-sensitive match (-i) - assert_equal [['Fruit', [[0, 5]]]], - FZF::ExtendedFuzzyMatcher.new(0).match(%w[Fruit Grapefruit], q, '', '').sort - - # Case-insensitive match (+i) - assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]], - FZF::ExtendedFuzzyMatcher.new(Regexp::IGNORECASE). - match(%w[Fruit Grapefruit], q, '', '').sort - end - end - - def test_extended_fuzzy_matcher - matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE - list = %w[ - juice - juiceful - juiceless - juicily - juiciness - juicy - _juice] - match = proc { |q, prefix| - matcher.match(list, q, prefix, '').sort.map { |p| [p.first, p.last.sort] } - } - - assert matcher.caches.empty? - 3.times do - ['y j', 'j y'].each do |pat| - (0..pat.length - 1).each do |prefix_length| - prefix = pat[0, prefix_length] - assert_equal( - [["juicily", [[0, 1], [6, 7]]], - ["juicy", [[0, 1], [4, 5]]]], - match.call(pat, prefix)) - end - end - - # $ - assert_equal [["juiceful", [[7, 8]]]], match.call('l$', '') - assert_equal [["juiceful", [[7, 8]]], - ["juiceless", [[5, 6]]], - ["juicily", [[5, 6]]]], match.call('l', '') - - # ^ - assert_equal list.length, match.call('j', '').length - assert_equal list.length - 1, match.call('^j', '').length - - # ^ + $ - assert_equal 0, match.call('^juici$', '').length - assert_equal 1, match.call('^juice$', '').length - assert_equal 0, match.call('^.*$', '').length - - # ! - assert_equal 0, match.call('!j', '').length - - # ! + ^ - assert_equal [["_juice", []]], match.call('!^j', '') - - # ! + $ - assert_equal list.length - 1, match.call('!l$', '').length - - # ! + f - assert_equal [["juicy", [[4, 5]]]], match.call('y !l', '') - - # ' - assert_equal %w[juiceful juiceless juicily], - match.call('il', '').map { |e| e.first } - assert_equal %w[juicily], - match.call("'il", '').map { |e| e.first } - assert_equal (list - %w[juicily]).sort, - match.call("!'il", '').map { |e| e.first }.sort - end - assert !matcher.caches.empty? - end - - def test_xfuzzy_matcher_prefix_cache - matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE - list = %w[ - a.java - b.java - java.jive - c.java$ - d.java - ] - 2.times do - assert_equal 5, matcher.match(list, 'java', 'java', '').length - assert_equal 3, matcher.match(list, 'java$', 'java$', '').length - assert_equal 1, matcher.match(list, 'java$$', 'java$$', '').length - - assert_equal 0, matcher.match(list, '!java', '!java', '').length - assert_equal 4, matcher.match(list, '!^jav', '!^jav', '').length - assert_equal 4, matcher.match(list, '!^java', '!^java', '').length - assert_equal 2, matcher.match(list, '!^java !b !c', '!^java', '').length - end - end - - def test_sort_by_rank - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE - xmatcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE - list = %w[ - 0____1 - 0_____1 - 01 - ____0_1 - 01_ - _01_ - 0______1 - ___01___ - ] - assert_equal( - [["01", [[0, 2]]], - ["01_", [[0, 2]]], - ["_01_", [[1, 3]]], - ["___01___", [[3, 5]]], - ["____0_1", [[4, 7]]], - ["0____1", [[0, 6]]], - ["0_____1", [[0, 7]]], - ["0______1", [[0, 8]]]], - FZF.sort(matcher.match(list, '01', '', ''))) - - assert_equal( - [["01", [[0, 1], [1, 2]]], - ["01_", [[0, 1], [1, 2]]], - ["_01_", [[1, 2], [2, 3]]], - ["0____1", [[0, 1], [5, 6]]], - ["0_____1", [[0, 1], [6, 7]]], - ["____0_1", [[4, 5], [6, 7]]], - ["0______1", [[0, 1], [7, 8]]], - ["___01___", [[3, 4], [4, 5]]]], - FZF.sort(xmatcher.match(list, '0 1', '', ''))) - - assert_equal( - [["_01_", [[1, 3], [0, 4]], [4, 4, "_01_"]], - ["___01___", [[3, 5], [0, 2]], [4, 8, "___01___"]], - ["____0_1", [[4, 7], [0, 2]], [5, 7, "____0_1"]], - ["0____1", [[0, 6], [1, 3]], [6, 6, "0____1"]], - ["0_____1", [[0, 7], [1, 3]], [7, 7, "0_____1"]], - ["0______1", [[0, 8], [1, 3]], [8, 8, "0______1"]]], - FZF.sort(xmatcher.match(list, '01 __', '', '')).map { |tuple| - tuple << FZF.rank(tuple) - } - ) - end - - def test_extended_exact_mode - exact = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :exact - fuzzy = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy - list = %w[ - extended-exact-mode-not-fuzzy - extended'-fuzzy-mode - ] - assert_equal 2, fuzzy.match(list, 'extended', '', '').length - assert_equal 2, fuzzy.match(list, 'mode extended', '', '').length - assert_equal 2, fuzzy.match(list, 'xtndd', '', '').length - assert_equal 2, fuzzy.match(list, "'-fuzzy", '', '').length - - assert_equal 2, exact.match(list, 'extended', '', '').length - assert_equal 2, exact.match(list, 'mode extended', '', '').length - assert_equal 0, exact.match(list, 'xtndd', '', '').length - assert_equal 1, exact.match(list, "'-fuzzy", '', '').length - assert_equal 2, exact.match(list, "-fuzzy", '', '').length - end - - # ^$ -> matches empty item - def test_format_empty_item - fzf = FZF.new [] - item = ['', [[0, 0]]] - line, offsets = item - tokens = fzf.format line, 80, offsets - assert_equal [], tokens - end - - def test_mouse_event - interval = FZF::MouseEvent::DOUBLE_CLICK_INTERVAL - me = FZF::MouseEvent.new nil - me.v = 10 - assert_equal false, me.double?(10) - assert_equal false, me.double?(20) - me.v = 20 - assert_equal false, me.double?(10) - assert_equal false, me.double?(20) - me.v = 20 - assert_equal false, me.double?(10) - assert_equal true, me.double?(20) - 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, [1..1] - 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, [2..2] - assert_equal [[list[0], [[19, 20]]]], matcher.match(list, 'r', '', '') - - # Comma-separated - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2..2, 0..0] - assert_equal [[list[0], [[19, 20]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '') - - # Ordered - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0, 2..2] - assert_equal [[list[0], [[3, 4]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '') - - regex = FZF.build_delim_regex "\t" - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0], regex - assert_equal [[list[0], [[3, 10]]]], matcher.match(list, 're', '', '') - - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], 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..-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, [0..0], regex - assert_equal [list[1]], matcher.match(list, 'f', '', '').map(&:first) - - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], regex - assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '') - end - - def test_nth_match_range - list = [ - ' first second third', - 'fourth fifth sixth', - ] - - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..2] - assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '') - assert_equal [], matcher.match(list, 'fo', '', '') - - matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..-1, 0..0] - assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '') - assert_equal [[list[1], [[0, 2]]]], matcher.match(list, 'fo', '', '') - - matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..0, 1..2] - assert_equal [], matcher.match(list, '^t', '', '') - - matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..1, 2..2] - assert_equal [[list[0], [[16, 17]]]], matcher.match(list, '^t', '', '') - - matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [1..-1] - assert_equal [[list[0], [[8, 9]]]], matcher.match(list, '^s', '', '') - end - - 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(@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 = stream_for '' - - def sorted_lines line - line.split($/).sort - end - - begin - tty = MockTTY.new - $stdout = output - 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 sorted_lines(expected), sorted_lines(output.string) - ensure - $stdout = STDOUT - end - end - - def test_filter - { - %w[--filter=ol] => 'World', - %w[--filter=ol --print-query] => "ol\nWorld", - }.each do |opts, expected| - assert_fzf_output opts, "Hello\nWorld", expected - end - end - - def test_select_1 - { - %w[--query=ol --select-1] => 'World', - %w[--query=ol --select-1 --print-query] => "ol\nWorld", - }.each do |opts, expected| - assert_fzf_output opts, "Hello\nWorld", expected - end - end - - def test_select_1_without_query - assert_fzf_output %w[--select-1], 'Hello World', 'Hello World' - end - - def test_select_1_ambiguity - begin - Timeout::timeout(0.5) do - assert_fzf_output %w[--query=o --select-1], "hello\nworld", "should not match" - end - rescue Timeout::Error - Curses.close_screen - end - end - - def test_exit_0 - { - %w[--query=zz --exit-0] => '', - %w[--query=zz --exit-0 --print-query] => 'zz', - }.each do |opts, expected| - assert_fzf_output opts, "Hello\nWorld", expected - end - end - - def test_exit_0_without_query - 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', - '1 2 3 4' - ] - assert_equal [ - ['1 2 3 4', [[0, 13], [16, 22]]], - ['1 3 4 2', [[0, 24], [12, 17]]], - ], FZF.sort(FZF::ExtendedFuzzyMatcher.new(nil).match(list, '12 34', '', '')) - end - - def test_constrain - fzf = FZF.new [] - - # [#**** ] - assert_equal [false, 0, 0], fzf.constrain(0, 0, 5, 100) - - # *****[**#** ... ] => [**#******* ... ] - assert_equal [true, 0, 2], fzf.constrain(5, 7, 10, 100) - - # [**********]**#** => ***[*********#]** - assert_equal [true, 3, 12], fzf.constrain(0, 12, 15, 10) - - # *****[**#** ] => ***[**#****] - assert_equal [true, 3, 5], fzf.constrain(5, 7, 10, 7) - - # *****[**#** ] => ****[**#***] - assert_equal [true, 4, 6], fzf.constrain(5, 7, 10, 6) - - # ***** [#] => ****[#] - assert_equal [true, 4, 4], fzf.constrain(10, 10, 5, 1) - - # [ ] #**** => [#]**** - assert_equal [true, 0, 0], fzf.constrain(-5, 0, 5, 1) - - # [ ] **#** => **[#]** - assert_equal [true, 2, 2], fzf.constrain(-5, 2, 5, 1) - - # [***** #] => [****# ] - assert_equal [true, 0, 4], fzf.constrain(0, 7, 5, 10) - - # **[***** #] => [******# ] - assert_equal [true, 0, 6], fzf.constrain(2, 10, 7, 10) - end - - def test_invalid_utf8 - tmp = Tempfile.new('fzf') - tmp << 'hello ' << [0xff].pack('C*') << ' world' << $/ << [0xff].pack('C*') - tmp.close - begin - 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 - - # ALT-D - assert_fzf_output %w[--print-query], "", "hello baby = world" do |tty| - tty << "hello world baby" - tty << alt(:b) << alt(:b) << alt(:d) - tty << ctrl(:e) << " = " << ctrl(:y) - tty << "\r" - end - - # ALT-BACKSPACE - assert_fzf_output %w[--print-query], "", "hello baby = world " do |tty| - tty << "hello world baby" - tty << alt(:b) << alt(127.chr) - tty << ctrl(:e) << " = " << ctrl(:y) - tty << "\r" - end - - # Word-movements - assert_fzf_output %w[--print-query], "", "ello!_orld!~ foo=?" do |tty| - tty << "hello_world==baby?" - tty << alt(:b) << ctrl(:d) - tty << alt(:b) << ctrl(:d) - tty << alt(:b) << ctrl(:d) - tty << alt(:f) << '!' - tty << alt(:f) << '!' - tty << alt(:d) << '~' - tty << " foo=bar foo=bar" - tty << ctrl(:w) - tty << alt(127.chr) - tty << "\r" - end - end - - def alt chr - "\e#{chr}" - end - - def ctrl char - char.to_s.ord - 'a'.ord + 1 - end -end - From 0f474d541dd08d5a0664ab52ffa5019bde0e5d7d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Mar 2015 15:07:34 +0900 Subject: [PATCH 123/408] Note on upgrade --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 22fce29..7cdb0de 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,16 @@ Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } ``` +#### Upgrading fzf + +fzf is being actively developed and you might want to upgrade it once in a +while. Please follow the instruction below depending on the installation +method. + +- git: `cd ~/.fzf && git pull && ./install` +- brew: `brew update && brew upgrade fzf && $(brew info fzf | grep /install)` +- vim-plug: `:PlugUpdate fzf` + Usage ----- From 68c5bea3f82cb0b9c29a298a1588838749054f4e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 12 Mar 2015 10:06:15 +0900 Subject: [PATCH 124/408] Fix install script for platforms w/o matching Go binary (#141) --- install | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/install b/install index 8b53f72..f1fae0b 100755 --- a/install +++ b/install @@ -77,7 +77,7 @@ case "$archi" in Darwin\ i*86) download fzf-$version-darwin_386 ;; Linux\ x86_64) download fzf-$version-linux_amd64 ;; Linux\ i*86) download fzf-$version-linux_386 ;; - *) binary_available=0 ;; + *) binary_available=0 binary_error=1 ;; esac cd "$fzf_base" @@ -173,6 +173,9 @@ fzf() { $fzf_cmd "\$@" } export -f fzf > /dev/null +if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then + export PATH="$fzf_base/bin:\$PATH" +fi # Auto-completion # --------------- From dd6138a6559363871d88c594ab9d170e36959129 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 13 Mar 2015 01:27:16 +0900 Subject: [PATCH 125/408] Fix #142, #144 - Improve CTRL-R for zsh --- install | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/install b/install index f1fae0b..b974afa 100755 --- a/install +++ b/install @@ -326,7 +326,12 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { - LBUFFER=$(fc -l 1 | fzf +s --tac +m -n2..,.. | sed "s/ *[0-9*]* *//") + local selected + if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. -q "$LBUFFER"); then + num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') + LBUFFER=!$num + zle expand-history + fi zle redisplay } zle -N fzf-history-widget From 3935aa84d8cc66c0371926c61afe89832f00df10 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 13 Mar 2015 13:08:42 +0900 Subject: [PATCH 126/408] Refactor shell extensions - Use symlinks instead of generating the full content - Update fish_user_paths and remove ~/.config/fish/functions/fzf.fish - Create wrapper script for fzf when Ruby version and use it instead of exported function not to break fzf-tmux --- fzf | 3 +- fzf-completion.zsh | 9 - install | 310 ++----------------- fzf-completion.bash => shell/completion.bash | 0 shell/key-bindings.bash | 75 +++++ shell/key-bindings.fish | 79 +++++ shell/key-bindings.zsh | 59 ++++ uninstall | 1 + 8 files changed, 249 insertions(+), 287 deletions(-) delete mode 100644 fzf-completion.zsh rename fzf-completion.bash => shell/completion.bash (100%) create mode 100644 shell/key-bindings.bash create mode 100644 shell/key-bindings.fish create mode 100644 shell/key-bindings.zsh diff --git a/fzf b/fzf index eb36391..28518ae 100755 --- a/fzf +++ b/fzf @@ -623,7 +623,8 @@ class FZF def start_reader stream = if @source.tty? - if default_command = ENV['FZF_DEFAULT_COMMAND'] + default_command = ENV['FZF_DEFAULT_COMMAND'] + if default_command && !default_command.empty? IO.popen(default_command) elsif !`which find`.empty? IO.popen("find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null") diff --git a/fzf-completion.zsh b/fzf-completion.zsh deleted file mode 100644 index 5e2db28..0000000 --- a/fzf-completion.zsh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/zsh -# ____ ____ -# / __/___ / __/ -# / /_/_ / / /_ -# / __/ / /_/ __/ -# /_/ /___/_/-completion.zsh -# - -# TODO diff --git a/install b/install index b974afa..9871eab 100755 --- a/install +++ b/install @@ -144,6 +144,14 @@ if [ -n "$binary_error" ]; then echo "< 1.9" fzf_cmd="$ruby $fzf_base/fzf" fi + + # Create fzf script + echo -n "Creating wrapper script for fzf ... " + rm -f "$fzf_base"/bin/fzf + echo "#!/bin/sh" > "$fzf_base"/bin/fzf + echo "$fzf_cmd \"\$@\"" >> "$fzf_base"/bin/fzf + chmod +x "$fzf_base"/bin/fzf + echo "OK" fi # Auto-completion @@ -159,35 +167,19 @@ for shell in bash zsh; do echo -n "Generate ~/.fzf.$shell ... " src=~/.fzf.${shell} - fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/fzf-completion.${shell}\"" - if [ $auto_completion -ne 0 ]; then + fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\"" + if [ $shell != bash -o $auto_completion -ne 0 ]; then fzf_completion="# $fzf_completion" fi - if [ -n "$binary_error" ]; then - cat > $src << EOF -# Setup fzf function -# ------------------ -unalias fzf 2> /dev/null -fzf() { - $fzf_cmd "\$@" -} -export -f fzf > /dev/null -if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then - export PATH="$fzf_base/bin:\$PATH" -fi + fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\"" + if [ $key_bindings -ne 0 ]; then + fzf_key_bindings="# $fzf_key_bindings" + fi -# Auto-completion -# --------------- -$fzf_completion - -EOF - else - cat > $src << EOF + cat > $src << EOF # Setup fzf # --------- -unalias fzf 2> /dev/null -unset fzf 2> /dev/null if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then export PATH="$fzf_base/bin:\$PATH" fi @@ -196,265 +188,35 @@ fi # --------------- $fzf_completion +# Key bindings +# ------------ +$fzf_key_bindings + EOF - fi - - if [ $key_bindings -eq 0 ]; then - if [ $shell = bash ]; then - cat >> $src << "EOFZF" -# Key bindings -# ------------ -__fsel() { - command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ - -o -type f -print \ - -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do - printf '%q ' "$item" - done - echo -} - -if [[ $- =~ i ]]; then - -__fsel_tmux() { - local height - height=${FZF_TMUX_HEIGHT:-40%} - if [[ $height =~ %$ ]]; then - height="-p ${height%\%}" - else - height="-l $height" - fi - tmux split-window $height "bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'" -} - -__fcd() { - local dir - dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ - -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m) && printf 'cd %q' "$dir" -} - -__use_tmux=0 -[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1 - -if [ -z "$(set -o | \grep '^vi.*on')" ]; then - # Required to refresh the prompt after fzf - bind '"\er": redraw-current-line' - - # CTRL-T - Paste the selected file path into the command line - if [ $__use_tmux -eq 1 ]; then - bind '"\C-t": " \C-u \C-a\C-k$(__fsel_tmux)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' - else - bind '"\C-t": " \C-u \C-a\C-k$(__fsel)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' - fi - - # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"' - - # ALT-C - cd into the selected directory - bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' -else - bind '"\C-x\C-e": shell-expand-line' - bind '"\C-x\C-r": redraw-current-line' - - # CTRL-T - Paste the selected file path into the command line - # - FIXME: Selected items are attached to the end regardless of cursor position - if [ $__use_tmux -eq 1 ]; then - bind '"\C-t": "\e$a \eddi$(__fsel_tmux)\C-x\C-e\e0P$xa"' - else - bind '"\C-t": "\e$a \eddi$(__fsel)\C-x\C-e\e0Px$a \C-x\C-r\exa "' - fi - bind -m vi-command '"\C-t": "i\C-t"' - - # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' - bind -m vi-command '"\C-r": "i\C-r"' - - # ALT-C - cd into the selected directory - bind '"\ec": "\eddi$(__fcd)\C-x\C-e\C-x\C-r\C-m"' - bind -m vi-command '"\ec": "i\ec"' -fi - -unset __use_tmux - -fi -EOFZF - else # zsh - cat >> $src << "EOFZF" -# Key bindings -# ------------ -# CTRL-T - Paste the selected file path(s) into the command line -__fsel() { - command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ - -o -type f -print \ - -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do - printf '%q ' "$item" - done - echo -} - -if [[ $- =~ i ]]; then - -if [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then - fzf-file-widget() { - local height - height=${FZF_TMUX_HEIGHT:-40%} - if [[ $height =~ %$ ]]; then - height="-p ${height%\%}" - else - height="-l $height" - fi - tmux split-window $height "zsh -c 'source ~/.fzf.zsh; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'" - } -else - fzf-file-widget() { - LBUFFER="${LBUFFER}$(__fsel)" - zle redisplay - } -fi -zle -N fzf-file-widget -bindkey '^T' fzf-file-widget - -# ALT-C - cd into the selected directory -fzf-cd-widget() { - cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ - -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m):-.}" - zle reset-prompt -} -zle -N fzf-cd-widget -bindkey '\ec' fzf-cd-widget - -# CTRL-R - Paste the selected command from history into the command line -fzf-history-widget() { - local selected - if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. -q "$LBUFFER"); then - num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') - LBUFFER=!$num - zle expand-history - fi - zle redisplay -} -zle -N fzf-history-widget -bindkey '^R' fzf-history-widget - -fi -EOFZF - fi - fi - echo "OK" done # fish has_fish=0 -if [ -n "$(which fish)" ]; then +if [ -n "$(which fish 2> /dev/null)" ]; then has_fish=1 - echo -n "Generate ~/.config/fish/functions/fzf.fish ... " + echo -n "Update fish_user_paths ... " + fish << EOF + echo \$fish_user_paths | grep $fzf_base/bin > /dev/null + or set --universal fish_user_paths \$fish_user_paths $fzf_base/bin +EOF + [ $? -eq 0 ] && echo "OK" || echo "Failed" + mkdir -p ~/.config/fish/functions - if [ -n "$binary_error" ]; then - cat > ~/.config/fish/functions/fzf.fish << EOFZF -function fzf - $fzf_cmd \$argv -end -EOFZF - else - cat > ~/.config/fish/functions/fzf.fish << EOFZF -function fzf - $fzf_base/bin/fzf \$argv -end -EOFZF + if [ -e ~/.config/fish/functions/fzf.fish ]; then + echo -n "Remove unnecessary ~/.config/fish/functions/fzf.fish ... " + rm -f ~/.config/fish/functions/fzf.fish && echo "OK" || echo "Failed" fi - echo "OK" if [ $key_bindings -eq 0 ]; then - echo -n "Generate ~/.config/fish/functions/fzf_key_bindings.fish ... " - cat > ~/.config/fish/functions/fzf_key_bindings.fish << "EOFZF" -function fzf_key_bindings - # Due to a bug of fish, we cannot use command substitution, - # so we use temporary file instead - if [ -z "$TMPDIR" ] - set -g TMPDIR /tmp - end - - function __fzf_list - command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ - -o -type f -print \ - -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3- - end - - function __fzf_list_dir - command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \ - -prune -o -type d -print 2> /dev/null | sed 1d | cut -b3- - end - - function __fzf_escape - while read item - echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' ' - end - end - - function __fzf_ctrl_t - if [ -n "$TMUX_PANE" -a "$FZF_TMUX" != "0" ] - tmux split-window (__fzf_tmux_height) "fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'" - else - __fzf_list | fzf -m > $TMPDIR/fzf.result - and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) - commandline -f repaint - rm -f $TMPDIR/fzf.result - end - end - - function __fzf_ctrl_t_tmux - __fzf_list | fzf -m > $TMPDIR/fzf.result - and tmux send-keys -t $argv[1] (cat $TMPDIR/fzf.result | __fzf_escape) - rm -f $TMPDIR/fzf.result - end - - function __fzf_reverse - if which tac > /dev/null - tac $argv - else - tail -r $argv - end - end - - function __fzf_ctrl_r - history | __fzf_reverse | fzf +s --tac +m > $TMPDIR/fzf.result - and commandline (cat $TMPDIR/fzf.result) - commandline -f repaint - rm -f $TMPDIR/fzf.result - end - - function __fzf_alt_c - # Fish hangs if the command before pipe redirects (2> /dev/null) - __fzf_list_dir | fzf +m > $TMPDIR/fzf.result - [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] - and cd (cat $TMPDIR/fzf.result) - commandline -f repaint - rm -f $TMPDIR/fzf.result - end - - function __fzf_tmux_height - if set -q FZF_TMUX_HEIGHT - set height $FZF_TMUX_HEIGHT - else - set height 40% - end - if echo $height | \grep -q -E '%$' - echo "-p "(echo $height | sed 's/%$//') - else - echo "-l $height" - end - set -e height - end - - bind \ct '__fzf_ctrl_t' - bind \cr '__fzf_ctrl_r' - bind \ec '__fzf_alt_c' -end -EOFZF - echo "OK" + echo -n "Symlink ~/.config/fish/functions/fzf_key_bindings.fish ... " + ln -sf $fzf_base/shell/key-bindings.fish \ + ~/.config/fish/functions/fzf_key_bindings.fish && echo "OK" || echo "Failed" fi fi @@ -484,12 +246,6 @@ done if [ $key_bindings -eq 0 -a $has_fish -eq 1 ]; then bind_file=~/.config/fish/functions/fish_user_key_bindings.fish append_line "fzf_key_bindings" "$bind_file" - - echo ' * Due to a known bug of fish, you may have issues running fzf on fish.' - echo ' * If that happens, try the following:' - echo ' - Remove ~/.config/fish/functions/fzf.fish' - echo ' - Place fzf executable in a directory included in $PATH' - echo fi cat << EOF diff --git a/fzf-completion.bash b/shell/completion.bash similarity index 100% rename from fzf-completion.bash rename to shell/completion.bash diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash new file mode 100644 index 0000000..b7c7946 --- /dev/null +++ b/shell/key-bindings.bash @@ -0,0 +1,75 @@ +# Key bindings +# ------------ +__fsel() { + command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + -o -type f -print \ + -o -type d -print \ + -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do + printf '%q ' "$item" + done + echo +} + +if [[ $- =~ i ]]; then + +__fsel_tmux() { + local height + height=${FZF_TMUX_HEIGHT:-40%} + if [[ $height =~ %$ ]]; then + height="-p ${height%\%}" + else + height="-l $height" + fi + tmux split-window $height "bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'" +} + +__fcd() { + local dir + dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m) && printf 'cd %q' "$dir" +} + +__use_tmux=0 +[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1 + +if [ -z "$(set -o | \grep '^vi.*on')" ]; then + # Required to refresh the prompt after fzf + bind '"\er": redraw-current-line' + + # CTRL-T - Paste the selected file path into the command line + if [ $__use_tmux -eq 1 ]; then + bind '"\C-t": " \C-u \C-a\C-k$(__fsel_tmux)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' + else + bind '"\C-t": " \C-u \C-a\C-k$(__fsel)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' + fi + + # CTRL-R - Paste the selected command from history into the command line + bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"' + + # ALT-C - cd into the selected directory + bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' +else + bind '"\C-x\C-e": shell-expand-line' + bind '"\C-x\C-r": redraw-current-line' + + # CTRL-T - Paste the selected file path into the command line + # - FIXME: Selected items are attached to the end regardless of cursor position + if [ $__use_tmux -eq 1 ]; then + bind '"\C-t": "\e$a \eddi$(__fsel_tmux)\C-x\C-e\e0P$xa"' + else + bind '"\C-t": "\e$a \eddi$(__fsel)\C-x\C-e\e0Px$a \C-x\C-r\exa "' + fi + bind -m vi-command '"\C-t": "i\C-t"' + + # CTRL-R - Paste the selected command from history into the command line + bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' + bind -m vi-command '"\C-r": "i\C-r"' + + # ALT-C - cd into the selected directory + bind '"\ec": "\eddi$(__fcd)\C-x\C-e\C-x\C-r\C-m"' + bind -m vi-command '"\ec": "i\ec"' +fi + +unset __use_tmux + +fi diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish new file mode 100644 index 0000000..be39e3d --- /dev/null +++ b/shell/key-bindings.fish @@ -0,0 +1,79 @@ +# Key bindings +# ------------ +function fzf_key_bindings + # Due to a bug of fish, we cannot use command substitution, + # so we use temporary file instead + if [ -z "$TMPDIR" ] + set -g TMPDIR /tmp + end + + function __fzf_list + command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + -o -type f -print \ + -o -type d -print \ + -o -type l -print 2> /dev/null | sed 1d | cut -b3- + end + + function __fzf_list_dir + command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \ + -prune -o -type d -print 2> /dev/null | sed 1d | cut -b3- + end + + function __fzf_escape + while read item + echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' ' + end + end + + function __fzf_ctrl_t + if [ -n "$TMUX_PANE" -a "$FZF_TMUX" != "0" ] + tmux split-window (__fzf_tmux_height) "fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'" + else + __fzf_list | fzf -m > $TMPDIR/fzf.result + and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) + commandline -f repaint + rm -f $TMPDIR/fzf.result + end + end + + function __fzf_ctrl_t_tmux + __fzf_list | fzf -m > $TMPDIR/fzf.result + and tmux send-keys -t $argv[1] (cat $TMPDIR/fzf.result | __fzf_escape) + rm -f $TMPDIR/fzf.result + end + + function __fzf_ctrl_r + history | fzf +s +m > $TMPDIR/fzf.result + and commandline (cat $TMPDIR/fzf.result) + commandline -f repaint + rm -f $TMPDIR/fzf.result + end + + function __fzf_alt_c + # Fish hangs if the command before pipe redirects (2> /dev/null) + __fzf_list_dir | fzf +m > $TMPDIR/fzf.result + [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] + and cd (cat $TMPDIR/fzf.result) + commandline -f repaint + rm -f $TMPDIR/fzf.result + end + + function __fzf_tmux_height + if set -q FZF_TMUX_HEIGHT + set height $FZF_TMUX_HEIGHT + else + set height 40% + end + if echo $height | \grep -q -E '%$' + echo "-p "(echo $height | sed 's/%$//') + else + echo "-l $height" + end + set -e height + end + + bind \ct '__fzf_ctrl_t' + bind \cr '__fzf_ctrl_r' + bind \ec '__fzf_alt_c' +end + diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh new file mode 100644 index 0000000..84ba730 --- /dev/null +++ b/shell/key-bindings.zsh @@ -0,0 +1,59 @@ +# Key bindings +# ------------ +# CTRL-T - Paste the selected file path(s) into the command line +__fsel() { + command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + -o -type f -print \ + -o -type d -print \ + -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do + printf '%q ' "$item" + done + echo +} + +if [[ $- =~ i ]]; then + +if [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then + fzf-file-widget() { + local height + height=${FZF_TMUX_HEIGHT:-40%} + if [[ $height =~ %$ ]]; then + height="-p ${height%\%}" + else + height="-l $height" + fi + tmux split-window $height "zsh -c 'source ~/.fzf.zsh; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'" + } +else + fzf-file-widget() { + LBUFFER="${LBUFFER}$(__fsel)" + zle redisplay + } +fi +zle -N fzf-file-widget +bindkey '^T' fzf-file-widget + +# ALT-C - cd into the selected directory +fzf-cd-widget() { + cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m):-.}" + zle reset-prompt +} +zle -N fzf-cd-widget +bindkey '\ec' fzf-cd-widget + +# CTRL-R - Paste the selected command from history into the command line +fzf-history-widget() { + local selected + if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. -q "$LBUFFER"); then + num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') + LBUFFER=!$num + zle expand-history + fi + zle redisplay +} +zle -N fzf-history-widget +bindkey '^R' fzf-history-widget + +fi + diff --git a/uninstall b/uninstall index 1884c47..e2717a3 100755 --- a/uninstall +++ b/uninstall @@ -68,6 +68,7 @@ fi if [ -d ~/.config/fish/functions ]; then remove ~/.config/fish/functions/fzf.fish + remove ~/.config/fish/functions/fzf_key_bindings.fish if [ "$(ls -A ~/.config/fish/functions)" ]; then echo "Can't delete non-empty directory: \"~/.config/fish/functions\"" From 4977174deffb631207b90753c5d451fbe438ed18 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 13 Mar 2015 22:45:28 +0900 Subject: [PATCH 127/408] [fzf-mux] Remove unnecessary env var from command --- bin/fzf-tmux | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 324562a..ffb98d9 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -107,7 +107,9 @@ fail() { exit 1 } fzf=$(which fzf 2> /dev/null) || fail "fzf executable not found" -envs="FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" +envs="" +[ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" +[ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" mkfifo $fifo2 mkfifo $fifo3 From c04e8de9b008428fee658e00661983ae22430435 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 13 Mar 2015 22:46:53 +0900 Subject: [PATCH 128/408] Make sure to start tmux pane from the current directory (#143) - fzf-tmux - CTRL-T of bash/zsh/fish - fish implementation may not work if the path contains double-quote characters (FIXME) --- bin/fzf-tmux | 2 +- shell/key-bindings.bash | 2 +- shell/key-bindings.fish | 3 ++- shell/key-bindings.zsh | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index ffb98d9..f1fcb09 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -115,7 +115,7 @@ mkfifo $fifo2 mkfifo $fifo3 if [ -n "$term" -o -t 0 ]; then tmux set-window-option -q synchronize-panes off \;\ - split-window $opt "$envs"' sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "cd $(printf %q "$PWD");$envs"' sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap else mkfifo $fifo1 tmux set-window-option -q synchronize-panes off \;\ diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index b7c7946..9011247 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -20,7 +20,7 @@ __fsel_tmux() { else height="-l $height" fi - tmux split-window $height "bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'" + tmux split-window $height "cd $(printf %q "$PWD");bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'" } __fcd() { diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index be39e3d..ce1eea7 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -27,7 +27,8 @@ function fzf_key_bindings function __fzf_ctrl_t if [ -n "$TMUX_PANE" -a "$FZF_TMUX" != "0" ] - tmux split-window (__fzf_tmux_height) "fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'" + # FIXME need to handle directory with double-quotes + tmux split-window (__fzf_tmux_height) "cd \"$PWD\";fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'" else __fzf_list | fzf -m > $TMPDIR/fzf.result and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 84ba730..6eb8083 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -22,7 +22,7 @@ if [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then else height="-l $height" fi - tmux split-window $height "zsh -c 'source ~/.fzf.zsh; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'" + tmux split-window $height "cd $(printf %q "$PWD");zsh -c 'source ~/.fzf.zsh; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'" } else fzf-file-widget() { From bc2e82efc13f4fb5ad714a7de98a24dfb74b36e9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 13 Mar 2015 23:04:13 +0900 Subject: [PATCH 129/408] [vim] Suppress error message when clear command is N/A --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 076ab91..713dfff 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -171,7 +171,7 @@ endfunction function! s:execute(dict, command, temps) call s:pushd(a:dict) - silent !clear + silent! !clear 2> /dev/null if has('gui_running') let launcher = get(a:dict, 'launcher', get(g:, 'fzf_launcher', s:launcher)) let command = printf(launcher, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'") From 7bb75b02136ce77359116b373856867957d5c5b4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Mar 2015 10:57:09 +0900 Subject: [PATCH 130/408] Update README --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7cdb0de..e193715 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,22 @@ -fzf - Fuzzy finder for your shell +fzf - a command-line fuzzy finder ================================= -fzf is a general-purpose fuzzy finder for your shell. +fzf is a general-purpose command-line fuzzy finder. ![](https://raw.github.com/junegunn/i/master/fzf.gif) -It was heavily inspired by [ctrlp.vim](https://github.com/kien/ctrlp.vim) and -the likes. +Pros +---- + +- No dependency +- Blazingly fast + - e.g. `locate / | fzf` +- Flexible layout + - Runs in fullscreen or in horizontal/vertical split using tmux +- The most comprehensive feature set + - Try `fzf --help` and be surprised +- Batteries included + - Vim plugin, key bindings and fuzzy auto-completion Installation ------------ From 2bebd5cdb44cfa28e015fce2591d5af7733a9fdb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 16 Mar 2015 02:49:09 +0900 Subject: [PATCH 131/408] Update README with fzf image --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e193715..f7f001a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -fzf - a command-line fuzzy finder -================================= +fzf - a command-line fuzzy finder [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) +=== fzf is a general-purpose command-line fuzzy finder. From d80a41bb6d1a507d65885d553a30d4e7dc7d0453 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 Mar 2015 02:19:05 +0900 Subject: [PATCH 132/408] Update README Use --depth option to avoid pulling devel branches --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7f001a..65d5d44 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Clone this repository and run [install](https://github.com/junegunn/fzf/blob/master/install) script. ```sh -git clone https://github.com/junegunn/fzf.git ~/.fzf +git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install ``` From e70a2a5817586e4e7df0ee1446f609bbd859164a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 19 Mar 2015 01:59:14 +0900 Subject: [PATCH 133/408] Add support for ANSI color codes --- src/ansi.go | 141 ++++++++++++++++++++++++++++++++++++++ src/ansi_test.go | 91 ++++++++++++++++++++++++ src/core.go | 33 ++++++++- src/curses/curses.go | 34 ++++++--- src/curses/curses_test.go | 14 ++++ src/item.go | 84 +++++++++++++++++++++++ src/item_test.go | 30 ++++++++ src/options.go | 7 ++ src/pattern.go | 1 + src/terminal.go | 29 ++++---- src/util/util.go | 11 +++ 11 files changed, 451 insertions(+), 24 deletions(-) create mode 100644 src/ansi.go create mode 100644 src/ansi_test.go create mode 100644 src/curses/curses_test.go diff --git a/src/ansi.go b/src/ansi.go new file mode 100644 index 0000000..650a374 --- /dev/null +++ b/src/ansi.go @@ -0,0 +1,141 @@ +package fzf + +import ( + "bytes" + "regexp" + "strconv" + "strings" +) + +type AnsiOffset struct { + offset [2]int32 + color ansiState +} + +type ansiState struct { + fg int + bg int + bold bool +} + +func (s *ansiState) colored() bool { + return s.fg != -1 || s.bg != -1 || s.bold +} + +func (s *ansiState) equals(t *ansiState) bool { + if t == nil { + return !s.colored() + } + return s.fg == t.fg && s.bg == t.bg && s.bold == t.bold +} + +var ansiRegex *regexp.Regexp + +func init() { + ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*m") +} + +func ExtractColor(str *string) (*string, []AnsiOffset) { + offsets := make([]AnsiOffset, 0) + + var output bytes.Buffer + var state *ansiState + + idx := 0 + for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) { + output.WriteString((*str)[idx:offset[0]]) + newLen := int32(output.Len()) + newState := interpretCode((*str)[offset[0]:offset[1]], state) + + if !newState.equals(state) { + if state != nil { + // Update last offset + (&offsets[len(offsets)-1]).offset[1] = int32(output.Len()) + } + + if newState.colored() { + // Append new offset + state = newState + offsets = append(offsets, AnsiOffset{[2]int32{newLen, newLen}, *state}) + } else { + // Discard state + state = nil + } + } + + idx = offset[1] + } + + rest := (*str)[idx:] + if len(rest) > 0 { + output.WriteString(rest) + if state != nil { + // Update last offset + (&offsets[len(offsets)-1]).offset[1] = int32(output.Len()) + } + } + outputStr := output.String() + return &outputStr, offsets +} + +func interpretCode(ansiCode string, prevState *ansiState) *ansiState { + // State + var state *ansiState + if prevState == nil { + state = &ansiState{-1, -1, false} + } else { + state = &ansiState{prevState.fg, prevState.bg, prevState.bold} + } + + ptr := &state.fg + state256 := 0 + + init := func() { + state.fg = -1 + state.bg = -1 + state.bold = false + state256 = 0 + } + + ansiCode = ansiCode[2 : len(ansiCode)-1] + for _, code := range strings.Split(ansiCode, ";") { + if num, err := strconv.Atoi(code); err == nil { + switch state256 { + case 0: + switch num { + case 38: + ptr = &state.fg + state256++ + case 48: + ptr = &state.bg + state256++ + case 39: + state.fg = -1 + case 49: + state.bg = -1 + case 1: + state.bold = true + case 0: + init() + default: + if num >= 30 && num <= 37 { + state.fg = num - 30 + } else if num >= 40 && num <= 47 { + state.bg = num - 40 + } + } + case 1: + switch num { + case 5: + state256++ + default: + state256 = 0 + } + case 2: + *ptr = num + state256 = 0 + } + } + } + return state +} diff --git a/src/ansi_test.go b/src/ansi_test.go new file mode 100644 index 0000000..37196dd --- /dev/null +++ b/src/ansi_test.go @@ -0,0 +1,91 @@ +package fzf + +import ( + "fmt" + "testing" +) + +func TestExtractColor(t *testing.T) { + assert := func(offset AnsiOffset, b int32, e int32, fg int, bg int, bold bool) { + if offset.offset[0] != b || offset.offset[1] != e || + offset.color.fg != fg || offset.color.bg != bg || offset.color.bold != bold { + t.Error(offset, b, e, fg, bg, bold) + } + } + + src := "hello world" + clean := "\x1b[0m" + check := func(assertion func(ansiOffsets []AnsiOffset)) { + output, ansiOffsets := ExtractColor(&src) + if *output != "hello world" { + t.Errorf("Invalid output: {}", output) + } + fmt.Println(src, ansiOffsets, clean) + assertion(ansiOffsets) + } + + check(func(offsets []AnsiOffset) { + if len(offsets) > 0 { + t.Fail() + } + }) + + src = "\x1b[0mhello world" + check(func(offsets []AnsiOffset) { + if len(offsets) > 0 { + t.Fail() + } + }) + + src = "\x1b[1mhello world" + check(func(offsets []AnsiOffset) { + if len(offsets) != 1 { + t.Fail() + } + assert(offsets[0], 0, 11, -1, -1, true) + }) + + src = "hello \x1b[34;45;1mworld" + check(func(offsets []AnsiOffset) { + if len(offsets) != 1 { + t.Fail() + } + assert(offsets[0], 6, 11, 4, 5, true) + }) + + src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld" + check(func(offsets []AnsiOffset) { + if len(offsets) != 1 { + t.Fail() + } + assert(offsets[0], 6, 11, 4, 5, true) + }) + + src = "hello \x1b[34;45;1mwor\x1b[0mld" + check(func(offsets []AnsiOffset) { + if len(offsets) != 1 { + t.Fail() + } + assert(offsets[0], 6, 9, 4, 5, true) + }) + + src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md" + check(func(offsets []AnsiOffset) { + if len(offsets) != 3 { + t.Fail() + } + assert(offsets[0], 6, 8, 4, 233, true) + assert(offsets[1], 8, 9, 161, 233, true) + assert(offsets[2], 10, 11, 161, -1, false) + }) + + // {38,48};5;{38,48} + src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md" + check(func(offsets []AnsiOffset) { + if len(offsets) != 2 { + t.Fail() + } + assert(offsets[0], 6, 9, 38, 48, true) + assert(offsets[1], 9, 10, 48, 38, true) + }) +} diff --git a/src/core.go b/src/core.go index 62190d0..d561ab4 100644 --- a/src/core.go +++ b/src/core.go @@ -63,14 +63,36 @@ func Run(options *Options) { // Event channel eventBox := util.NewEventBox() + // ANSI code processor + extractColors := func(data *string) (*string, []AnsiOffset) { + // By default, we do nothing + return data, nil + } + if opts.Ansi { + if opts.Color { + extractColors = func(data *string) (*string, []AnsiOffset) { + return ExtractColor(data) + } + } else { + // When color is disabled but ansi option is given, + // we simply strip out ANSI codes from the input + extractColors = func(data *string) (*string, []AnsiOffset) { + trimmed, _ := ExtractColor(data) + return trimmed, nil + } + } + } + // Chunk list var chunkList *ChunkList if len(opts.WithNth) == 0 { chunkList = NewChunkList(func(data *string, index int) *Item { + data, colors := extractColors(data) return &Item{ - text: data, - index: uint32(index), - rank: Rank{0, 0, uint32(index)}} + text: data, + index: uint32(index), + colors: colors, + rank: Rank{0, 0, uint32(index)}} }) } else { chunkList = NewChunkList(func(data *string, index int) *Item { @@ -79,7 +101,12 @@ func Run(options *Options) { text: Transform(tokens, opts.WithNth).whole, origText: data, index: uint32(index), + colors: nil, rank: Rank{0, 0, uint32(index)}} + + trimmed, colors := extractColors(item.text) + item.text = trimmed + item.colors = colors return &item }) } diff --git a/src/curses/curses.go b/src/curses/curses.go index 454f1e3..dfd7cf5 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -78,6 +78,7 @@ const ( ColInfo ColCursor ColSelected + ColUser ) const ( @@ -103,14 +104,17 @@ var ( _buf []byte _in *os.File _color func(int, bool) C.int + _colorMap map[int]int _prevDownTime time.Time _prevDownY int _clickY []int + DarkBG C.short ) func init() { _prevDownTime = time.Unix(0, 0) _clickY = []int{} + _colorMap = make(map[int]int) } func attrColored(pair int, bold bool) C.int { @@ -200,23 +204,25 @@ func Init(color bool, color256 bool, black bool, mouse bool) { bg = -1 } if color256 { + DarkBG = 236 C.init_pair(ColPrompt, 110, bg) C.init_pair(ColMatch, 108, bg) - C.init_pair(ColCurrent, 254, 236) - C.init_pair(ColCurrentMatch, 151, 236) + C.init_pair(ColCurrent, 254, DarkBG) + C.init_pair(ColCurrentMatch, 151, DarkBG) C.init_pair(ColSpinner, 148, bg) C.init_pair(ColInfo, 144, bg) - C.init_pair(ColCursor, 161, 236) - C.init_pair(ColSelected, 168, 236) + C.init_pair(ColCursor, 161, DarkBG) + C.init_pair(ColSelected, 168, DarkBG) } else { + DarkBG = C.COLOR_BLACK C.init_pair(ColPrompt, C.COLOR_BLUE, bg) C.init_pair(ColMatch, C.COLOR_GREEN, bg) - C.init_pair(ColCurrent, C.COLOR_YELLOW, C.COLOR_BLACK) - C.init_pair(ColCurrentMatch, C.COLOR_GREEN, C.COLOR_BLACK) + C.init_pair(ColCurrent, C.COLOR_YELLOW, DarkBG) + C.init_pair(ColCurrentMatch, C.COLOR_GREEN, DarkBG) C.init_pair(ColSpinner, C.COLOR_GREEN, bg) C.init_pair(ColInfo, C.COLOR_WHITE, bg) - C.init_pair(ColCursor, C.COLOR_RED, C.COLOR_BLACK) - C.init_pair(ColSelected, C.COLOR_MAGENTA, C.COLOR_BLACK) + C.init_pair(ColCursor, C.COLOR_RED, DarkBG) + C.init_pair(ColSelected, C.COLOR_MAGENTA, DarkBG) } _color = attrColored } else { @@ -428,3 +434,15 @@ func Endwin() { func Refresh() { C.refresh() } + +func PairFor(fg int, bg int) int { + key := (fg << 8) + bg + if found, prs := _colorMap[key]; prs { + return found + } + + id := len(_colorMap) + ColUser + C.init_pair(C.short(id), C.short(fg), C.short(bg)) + _colorMap[key] = id + return id +} diff --git a/src/curses/curses_test.go b/src/curses/curses_test.go new file mode 100644 index 0000000..db75c40 --- /dev/null +++ b/src/curses/curses_test.go @@ -0,0 +1,14 @@ +package curses + +import ( + "testing" +) + +func TestPairFor(t *testing.T) { + if PairFor(30, 50) != PairFor(30, 50) { + t.Fail() + } + if PairFor(-1, 10) != PairFor(-1, 10) { + t.Fail() + } +} diff --git a/src/item.go b/src/item.go index 2b8a9d1..f9a464f 100644 --- a/src/item.go +++ b/src/item.go @@ -1,8 +1,18 @@ package fzf +import ( + "github.com/junegunn/fzf/src/curses" +) + // Offset holds two 32-bit integers denoting the offsets of a matched substring type Offset [2]int32 +type ColorOffset struct { + offset [2]int32 + color int + bold bool +} + // Item represents each input line type Item struct { text *string @@ -10,6 +20,7 @@ type Item struct { transformed *Transformed index uint32 offsets []Offset + colors []AnsiOffset rank Rank } @@ -55,6 +66,79 @@ func (i *Item) AsString() string { return *i.text } +func (item *Item) ColorOffsets(color int, bold bool, current bool) []ColorOffset { + if len(item.colors) == 0 { + offsets := make([]ColorOffset, 0) + for _, off := range item.offsets { + offsets = append(offsets, ColorOffset{offset: off, color: color, bold: bold}) + } + return offsets + } + + // Find max column + var maxCol int32 = 0 + for _, off := range item.offsets { + if off[1] > maxCol { + maxCol = off[1] + } + } + for _, ansi := range item.colors { + if ansi.offset[1] > maxCol { + maxCol = ansi.offset[1] + } + } + cols := make([]int, maxCol) + + for colorIndex, ansi := range item.colors { + for i := ansi.offset[0]; i < ansi.offset[1]; i++ { + cols[i] = colorIndex + 1 // XXX + } + } + + for _, off := range item.offsets { + for i := off[0]; i < off[1]; i++ { + cols[i] = -1 + } + } + + // sort.Sort(ByOrder(offsets)) + + // Merge offsets + // ------------ ---- -- ---- + // ++++++++ ++++++++++ + // --++++++++-- --++++++++++--- + curr := 0 + start := 0 + offsets := make([]ColorOffset, 0) + add := func(idx int) { + if curr != 0 && idx > start { + if curr == -1 { + offsets = append(offsets, ColorOffset{ + offset: Offset{int32(start), int32(idx)}, color: color, bold: bold}) + } else { + ansi := item.colors[curr-1] + bg := ansi.color.bg + if current { + bg = int(curses.DarkBG) + } + offsets = append(offsets, ColorOffset{ + offset: Offset{int32(start), int32(idx)}, + color: curses.PairFor(ansi.color.fg, bg), + bold: ansi.color.bold || bold}) + } + } + } + for idx, col := range cols { + if col != curr { + add(idx) + start = idx + curr = col + } + } + add(int(maxCol)) + return offsets +} + // ByOrder is for sorting substring offsets type ByOrder []Offset diff --git a/src/item_test.go b/src/item_test.go index 372ab4a..0249edf 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -3,6 +3,8 @@ package fzf import ( "sort" "testing" + + "github.com/junegunn/fzf/src/curses" ) func TestOffsetSort(t *testing.T) { @@ -72,3 +74,31 @@ func TestItemRank(t *testing.T) { t.Error(items) } } + +func TestColorOffset(t *testing.T) { + // ------------ 20 ---- -- ---- + // ++++++++ ++++++++++ + // --++++++++-- --++++++++++--- + item := Item{ + offsets: []Offset{Offset{5, 15}, Offset{25, 35}}, + colors: []AnsiOffset{ + AnsiOffset{[2]int32{0, 20}, ansiState{1, 5, false}}, + AnsiOffset{[2]int32{22, 27}, ansiState{2, 6, true}}, + AnsiOffset{[2]int32{30, 32}, ansiState{3, 7, false}}, + AnsiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}} + // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] + + offsets := item.ColorOffsets(99, false, true) + assert := func(idx int, b int32, e int32, c int, bold bool) { + o := offsets[idx] + if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold { + t.Error(o) + } + } + assert(0, 0, 5, curses.ColUser, false) + assert(1, 5, 15, 99, false) + assert(2, 15, 20, curses.ColUser, false) + assert(3, 22, 25, curses.ColUser+1, true) + assert(4, 25, 35, 99, false) + assert(5, 35, 40, curses.ColUser+2, true) +} diff --git a/src/options.go b/src/options.go index dc8f0b8..573ce3d 100644 --- a/src/options.go +++ b/src/options.go @@ -29,6 +29,7 @@ const usage = `usage: fzf [options] Interface -m, --multi Enable multi-select with tab/shift-tab + --ansi Interpret ANSI color codes and remove from output --no-mouse Disable mouse +c, --no-color Disable colors +2, --no-256 Disable 256-color @@ -81,6 +82,7 @@ type Options struct { Sort int Tac bool Multi bool + Ansi bool Mouse bool Color bool Color256 bool @@ -106,6 +108,7 @@ func defaultOptions() *Options { Sort: 1000, Tac: false, Multi: false, + Ansi: false, Mouse: true, Color: true, Color256: strings.Contains(os.Getenv("TERM"), "256"), @@ -227,6 +230,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Multi = true case "+m", "--no-multi": opts.Multi = false + case "--ansi": + opts.Ansi = true + case "--no-ansi": + opts.Ansi = false case "--no-mouse": opts.Mouse = false case "+c", "--no-color": diff --git a/src/pattern.go b/src/pattern.go index 725ce2d..7acdbcf 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -264,6 +264,7 @@ func dupItem(item *Item, offsets []Offset) *Item { transformed: item.transformed, index: item.index, offsets: offsets, + colors: item.colors, rank: Rank{0, 0, item.index}} } diff --git a/src/terminal.go b/src/terminal.go index bd426d1..9402ef2 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -251,7 +251,7 @@ func (t *Terminal) printItem(item *Item, current bool) { } else { C.CPrint(C.ColCurrent, true, " ") } - t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch) + t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true) } else { C.CPrint(C.ColCursor, true, " ") if selected { @@ -259,7 +259,7 @@ func (t *Terminal) printItem(item *Item, current bool) { } else { C.Print(" ") } - t.printHighlighted(item, false, 0, C.ColMatch) + t.printHighlighted(item, false, 0, C.ColMatch, false) } } @@ -299,7 +299,7 @@ func trimLeft(runes []rune, width int) ([]rune, int32) { return runes, trimmed } -func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { +func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) { var maxe int32 for _, offset := range item.offsets { if offset[1] > maxe { @@ -309,7 +309,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { // Overflow text := []rune(*item.text) - offsets := item.offsets + offsets := item.ColorOffsets(col2, bold, current) maxWidth := C.MaxX() - 3 fullWidth := displayWidth(text) if fullWidth > maxWidth { @@ -328,37 +328,40 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { text, diff = trimLeft(text, maxWidth-2) // Transform offsets - offsets = make([]Offset, len(item.offsets)) - for idx, offset := range item.offsets { - b, e := offset[0], offset[1] + for idx, offset := range offsets { + b, e := offset.offset[0], offset.offset[1] b += 2 - diff e += 2 - diff b = util.Max32(b, 2) if b < e { - offsets[idx] = Offset{b, e} + offsets[idx].offset[0] = b + offsets[idx].offset[1] = e } } text = append([]rune(".."), text...) } } - sort.Sort(ByOrder(offsets)) var index int32 var substr string var prefixWidth int + maxOffset := int32(len(text)) for _, offset := range offsets { - b := util.Max32(index, offset[0]) - e := util.Max32(index, offset[1]) + b := util.Constrain32(offset.offset[0], index, maxOffset) + e := util.Constrain32(offset.offset[1], index, maxOffset) substr, prefixWidth = processTabs(text[index:b], prefixWidth) C.CPrint(col1, bold, substr) substr, prefixWidth = processTabs(text[b:e], prefixWidth) - C.CPrint(col2, bold, substr) + C.CPrint(offset.color, bold, substr) index = e + if index >= maxOffset { + break + } } - if index < int32(len(text)) { + if index < maxOffset { substr, _ = processTabs(text[index:], prefixWidth) C.CPrint(col1, bold, substr) } diff --git a/src/util/util.go b/src/util/util.go index 14833c0..1f53cc7 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -27,6 +27,17 @@ func Max32(first int32, second int32) int32 { return second } +// Constrain32 limits the given 32-bit integer with the upper and lower bounds +func Constrain32(val int32, min int32, max int32) int32 { + if val < min { + return min + } + if val > max { + return max + } + return val +} + // Constrain limits the given integer with the upper and lower bounds func Constrain(val int, min int, max int) int { if val < min { From 3dddbfd8fa45283b93c06f4263a30dfe6f7055f5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 19 Mar 2015 12:14:26 +0900 Subject: [PATCH 134/408] Fix string truncation --- src/terminal.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 9402ef2..ea0e826 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -333,10 +333,8 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, cur b += 2 - diff e += 2 - diff b = util.Max32(b, 2) - if b < e { - offsets[idx].offset[0] = b - offsets[idx].offset[1] = e - } + offsets[idx].offset[0] = b + offsets[idx].offset[1] = util.Max32(b, e) } text = append([]rune(".."), text...) } @@ -353,8 +351,10 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, cur substr, prefixWidth = processTabs(text[index:b], prefixWidth) C.CPrint(col1, bold, substr) - substr, prefixWidth = processTabs(text[b:e], prefixWidth) - C.CPrint(offset.color, bold, substr) + if b < e { + substr, prefixWidth = processTabs(text[b:e], prefixWidth) + C.CPrint(offset.color, bold, substr) + } index = e if index >= maxOffset { From a723977b9f9ac95fb8ff2596e098b4dbb6017fc4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 19 Mar 2015 13:06:20 +0900 Subject: [PATCH 135/408] Fix #149 - panic on empty string filter --- src/algo/algo.go | 12 ++++++++++++ src/algo/algo_test.go | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/src/algo/algo.go b/src/algo/algo.go index bc4e538..60c436e 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -12,6 +12,10 @@ import "strings" // FuzzyMatch performs fuzzy-match func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { + if len(pattern) == 0 { + return 0, 0 + } + runes := []rune(*input) // 0. (FIXME) How to find the shortest match? @@ -66,6 +70,10 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { // ExactMatchStrings performs exact-match using strings package. // Currently not used. func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, int) { + if len(pattern) == 0 { + return 0, 0 + } + var str string if caseSensitive { str = *input @@ -88,6 +96,10 @@ func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, // We might try to implement better algorithms in the future: // http://en.wikipedia.org/wiki/String_searching_algorithm func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, int) { + if len(pattern) == 0 { + return 0, 0 + } + runes := []rune(*input) numRunes := len(runes) plen := len(pattern) diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index 363b6ee..ac7aad5 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -42,3 +42,11 @@ func TestSuffixMatch(t *testing.T) { assertMatch(t, SuffixMatch, false, "fooBarbaz", "baz", 6, 9) assertMatch(t, SuffixMatch, true, "fooBarbaz", "Baz", -1, -1) } + +func TestEmptyPattern(t *testing.T) { + assertMatch(t, FuzzyMatch, true, "foobar", "", 0, 0) + assertMatch(t, ExactMatchStrings, true, "foobar", "", 0, 0) + assertMatch(t, ExactMatchNaive, true, "foobar", "", 0, 0) + assertMatch(t, PrefixMatch, true, "foobar", "", 0, 0) + assertMatch(t, SuffixMatch, true, "foobar", "", 6, 6) +} From 6130026786fce0a8ac9e053694761405f4f599c3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 19 Mar 2015 19:12:22 +0900 Subject: [PATCH 136/408] Bump up the version - 0.9.5 --- install | 2 +- src/constants.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install b/install index 9871eab..50f0076 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.4 +version=0.9.5 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/src/constants.go b/src/constants.go index f513853..729bc5c 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.4" +const Version = "0.9.5" // fzf events const ( From d94dfe087694d68073f01a51c7357fc4741641d8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 19 Mar 2015 19:59:38 +0900 Subject: [PATCH 137/408] Fix #151 - reduce initial memory footprint --- src/matcher.go | 4 ++++ src/merger.go | 24 +++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/matcher.go b/src/matcher.go index 0879a08..06352f5 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -134,6 +134,10 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } pattern := request.pattern empty := pattern.IsEmpty() + if empty { + return PassMerger(&request.chunks, m.tac), false + } + cancelled := util.NewAtomicBool(false) slices := m.sliceChunks(request.chunks) diff --git a/src/merger.go b/src/merger.go index 41323c1..21a7459 100644 --- a/src/merger.go +++ b/src/merger.go @@ -10,6 +10,7 @@ var EmptyMerger = NewMerger([][]*Item{}, false, false) type Merger struct { lists [][]*Item merged []*Item + chunks *[]*Chunk cursors []int sorted bool tac bool @@ -17,11 +18,24 @@ type Merger struct { count int } +func PassMerger(chunks *[]*Chunk, tac bool) *Merger { + mg := Merger{ + chunks: chunks, + tac: tac, + count: 0} + + for _, chunk := range *mg.chunks { + mg.count += len(*chunk) + } + return &mg +} + // NewMerger returns a new Merger func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger { mg := Merger{ lists: lists, merged: []*Item{}, + chunks: nil, cursors: make([]int, len(lists)), sorted: sorted, tac: tac, @@ -41,12 +55,20 @@ func (mg *Merger) Length() int { // Get returns the pointer to the Item object indexed by the given integer func (mg *Merger) Get(idx int) *Item { + if mg.chunks != nil { + if mg.tac { + idx = mg.count - idx - 1 + } + chunk := (*mg.chunks)[idx/ChunkSize] + return (*chunk)[idx%ChunkSize] + } + if mg.sorted { return mg.mergedGet(idx) } if mg.tac { - idx = mg.Length() - idx - 1 + idx = mg.count - idx - 1 } for _, list := range mg.lists { numItems := len(list) From b431e227da318931a4e7458f3cc07616c6b74ea4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Mar 2015 16:05:54 +0900 Subject: [PATCH 138/408] Code cleanup --- src/ansi.go | 8 ++++---- src/ansi_test.go | 22 +++++++++++----------- src/core.go | 14 +++++++------- src/item.go | 18 +++++++++--------- src/item_test.go | 12 ++++++------ src/matcher.go | 16 +++++----------- src/merger.go | 2 ++ src/terminal.go | 12 ++++++------ src/util/eventbox.go | 1 + 9 files changed, 51 insertions(+), 54 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index 650a374..fbfa050 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -7,7 +7,7 @@ import ( "strings" ) -type AnsiOffset struct { +type ansiOffset struct { offset [2]int32 color ansiState } @@ -35,8 +35,8 @@ func init() { ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*m") } -func ExtractColor(str *string) (*string, []AnsiOffset) { - offsets := make([]AnsiOffset, 0) +func extractColor(str *string) (*string, []ansiOffset) { + var offsets []ansiOffset var output bytes.Buffer var state *ansiState @@ -56,7 +56,7 @@ func ExtractColor(str *string) (*string, []AnsiOffset) { if newState.colored() { // Append new offset state = newState - offsets = append(offsets, AnsiOffset{[2]int32{newLen, newLen}, *state}) + offsets = append(offsets, ansiOffset{[2]int32{newLen, newLen}, *state}) } else { // Discard state state = nil diff --git a/src/ansi_test.go b/src/ansi_test.go index 37196dd..d5e0e43 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -6,7 +6,7 @@ import ( ) func TestExtractColor(t *testing.T) { - assert := func(offset AnsiOffset, b int32, e int32, fg int, bg int, bold bool) { + assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) { if offset.offset[0] != b || offset.offset[1] != e || offset.color.fg != fg || offset.color.bg != bg || offset.color.bold != bold { t.Error(offset, b, e, fg, bg, bold) @@ -15,8 +15,8 @@ func TestExtractColor(t *testing.T) { src := "hello world" clean := "\x1b[0m" - check := func(assertion func(ansiOffsets []AnsiOffset)) { - output, ansiOffsets := ExtractColor(&src) + check := func(assertion func(ansiOffsets []ansiOffset)) { + output, ansiOffsets := extractColor(&src) if *output != "hello world" { t.Errorf("Invalid output: {}", output) } @@ -24,21 +24,21 @@ func TestExtractColor(t *testing.T) { assertion(ansiOffsets) } - check(func(offsets []AnsiOffset) { + check(func(offsets []ansiOffset) { if len(offsets) > 0 { t.Fail() } }) src = "\x1b[0mhello world" - check(func(offsets []AnsiOffset) { + check(func(offsets []ansiOffset) { if len(offsets) > 0 { t.Fail() } }) src = "\x1b[1mhello world" - check(func(offsets []AnsiOffset) { + check(func(offsets []ansiOffset) { if len(offsets) != 1 { t.Fail() } @@ -46,7 +46,7 @@ func TestExtractColor(t *testing.T) { }) src = "hello \x1b[34;45;1mworld" - check(func(offsets []AnsiOffset) { + check(func(offsets []ansiOffset) { if len(offsets) != 1 { t.Fail() } @@ -54,7 +54,7 @@ func TestExtractColor(t *testing.T) { }) src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld" - check(func(offsets []AnsiOffset) { + check(func(offsets []ansiOffset) { if len(offsets) != 1 { t.Fail() } @@ -62,7 +62,7 @@ func TestExtractColor(t *testing.T) { }) src = "hello \x1b[34;45;1mwor\x1b[0mld" - check(func(offsets []AnsiOffset) { + check(func(offsets []ansiOffset) { if len(offsets) != 1 { t.Fail() } @@ -70,7 +70,7 @@ func TestExtractColor(t *testing.T) { }) src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md" - check(func(offsets []AnsiOffset) { + check(func(offsets []ansiOffset) { if len(offsets) != 3 { t.Fail() } @@ -81,7 +81,7 @@ func TestExtractColor(t *testing.T) { // {38,48};5;{38,48} src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md" - check(func(offsets []AnsiOffset) { + check(func(offsets []ansiOffset) { if len(offsets) != 2 { t.Fail() } diff --git a/src/core.go b/src/core.go index d561ab4..ebb7d66 100644 --- a/src/core.go +++ b/src/core.go @@ -64,20 +64,20 @@ func Run(options *Options) { eventBox := util.NewEventBox() // ANSI code processor - extractColors := func(data *string) (*string, []AnsiOffset) { + ansiProcessor := func(data *string) (*string, []ansiOffset) { // By default, we do nothing return data, nil } if opts.Ansi { if opts.Color { - extractColors = func(data *string) (*string, []AnsiOffset) { - return ExtractColor(data) + ansiProcessor = func(data *string) (*string, []ansiOffset) { + return extractColor(data) } } else { // When color is disabled but ansi option is given, // we simply strip out ANSI codes from the input - extractColors = func(data *string) (*string, []AnsiOffset) { - trimmed, _ := ExtractColor(data) + ansiProcessor = func(data *string) (*string, []ansiOffset) { + trimmed, _ := extractColor(data) return trimmed, nil } } @@ -87,7 +87,7 @@ func Run(options *Options) { var chunkList *ChunkList if len(opts.WithNth) == 0 { chunkList = NewChunkList(func(data *string, index int) *Item { - data, colors := extractColors(data) + data, colors := ansiProcessor(data) return &Item{ text: data, index: uint32(index), @@ -104,7 +104,7 @@ func Run(options *Options) { colors: nil, rank: Rank{0, 0, uint32(index)}} - trimmed, colors := extractColors(item.text) + trimmed, colors := ansiProcessor(item.text) item.text = trimmed item.colors = colors return &item diff --git a/src/item.go b/src/item.go index f9a464f..c94166f 100644 --- a/src/item.go +++ b/src/item.go @@ -7,7 +7,7 @@ import ( // Offset holds two 32-bit integers denoting the offsets of a matched substring type Offset [2]int32 -type ColorOffset struct { +type colorOffset struct { offset [2]int32 color int bold bool @@ -20,7 +20,7 @@ type Item struct { transformed *Transformed index uint32 offsets []Offset - colors []AnsiOffset + colors []ansiOffset rank Rank } @@ -66,17 +66,17 @@ func (i *Item) AsString() string { return *i.text } -func (item *Item) ColorOffsets(color int, bold bool, current bool) []ColorOffset { +func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset { if len(item.colors) == 0 { - offsets := make([]ColorOffset, 0) + var offsets []colorOffset for _, off := range item.offsets { - offsets = append(offsets, ColorOffset{offset: off, color: color, bold: bold}) + offsets = append(offsets, colorOffset{offset: off, color: color, bold: bold}) } return offsets } // Find max column - var maxCol int32 = 0 + var maxCol int32 for _, off := range item.offsets { if off[1] > maxCol { maxCol = off[1] @@ -109,11 +109,11 @@ func (item *Item) ColorOffsets(color int, bold bool, current bool) []ColorOffset // --++++++++-- --++++++++++--- curr := 0 start := 0 - offsets := make([]ColorOffset, 0) + var offsets []colorOffset add := func(idx int) { if curr != 0 && idx > start { if curr == -1 { - offsets = append(offsets, ColorOffset{ + offsets = append(offsets, colorOffset{ offset: Offset{int32(start), int32(idx)}, color: color, bold: bold}) } else { ansi := item.colors[curr-1] @@ -121,7 +121,7 @@ func (item *Item) ColorOffsets(color int, bold bool, current bool) []ColorOffset if current { bg = int(curses.DarkBG) } - offsets = append(offsets, ColorOffset{ + offsets = append(offsets, colorOffset{ offset: Offset{int32(start), int32(idx)}, color: curses.PairFor(ansi.color.fg, bg), bold: ansi.color.bold || bold}) diff --git a/src/item_test.go b/src/item_test.go index 0249edf..4eea8c1 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -81,14 +81,14 @@ func TestColorOffset(t *testing.T) { // --++++++++-- --++++++++++--- item := Item{ offsets: []Offset{Offset{5, 15}, Offset{25, 35}}, - colors: []AnsiOffset{ - AnsiOffset{[2]int32{0, 20}, ansiState{1, 5, false}}, - AnsiOffset{[2]int32{22, 27}, ansiState{2, 6, true}}, - AnsiOffset{[2]int32{30, 32}, ansiState{3, 7, false}}, - AnsiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}} + colors: []ansiOffset{ + ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}}, + ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}}, + ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}}, + ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}} // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] - offsets := item.ColorOffsets(99, false, true) + offsets := item.colorOffsets(99, false, true) assert := func(idx int, b int32, e int32, c int, bold bool) { o := offsets[idx] if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold { diff --git a/src/matcher.go b/src/matcher.go index 06352f5..a3a9bd0 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -133,8 +133,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { return EmptyMerger, false } pattern := request.pattern - empty := pattern.IsEmpty() - if empty { + if pattern.IsEmpty() { return PassMerger(&request.chunks, m.tac), false } @@ -152,19 +151,14 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { defer func() { waitGroup.Done() }() sliceMatches := []*Item{} for _, chunk := range chunks { - var matches []*Item - if empty { - matches = *chunk - } else { - matches = request.pattern.Match(chunk) - } + matches := request.pattern.Match(chunk) sliceMatches = append(sliceMatches, matches...) if cancelled.Get() { return } countChan <- len(matches) } - if !empty && m.sort { + if m.sort { if m.tac { sort.Sort(ByRelevanceTac(sliceMatches)) } else { @@ -191,7 +185,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { break } - if !empty && m.reqBox.Peek(reqReset) { + if m.reqBox.Peek(reqReset) { return nil, wait() } @@ -205,7 +199,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches } - return NewMerger(partialResults, !empty && m.sort, m.tac), false + return NewMerger(partialResults, m.sort, m.tac), false } // Reset is called to interrupt/signal the ongoing search diff --git a/src/merger.go b/src/merger.go index 21a7459..bc5d5de 100644 --- a/src/merger.go +++ b/src/merger.go @@ -18,6 +18,8 @@ type Merger struct { count int } +// PassMerger returns a new Merger that simply returns the items in the +// original order func PassMerger(chunks *[]*Chunk, tac bool) *Merger { mg := Merger{ chunks: chunks, diff --git a/src/terminal.go b/src/terminal.go index ea0e826..bf64ec7 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -47,17 +47,17 @@ type selectedItem struct { text *string } -type ByTimeOrder []selectedItem +type byTimeOrder []selectedItem -func (a ByTimeOrder) Len() int { +func (a byTimeOrder) Len() int { return len(a) } -func (a ByTimeOrder) Swap(i, j int) { +func (a byTimeOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByTimeOrder) Less(i, j int) bool { +func (a byTimeOrder) Less(i, j int) bool { return a[i].at.Before(a[j].at) } @@ -160,7 +160,7 @@ func (t *Terminal) output() { for _, sel := range t.selected { sels = append(sels, sel) } - sort.Sort(ByTimeOrder(sels)) + sort.Sort(byTimeOrder(sels)) for _, sel := range sels { fmt.Println(*sel.text) } @@ -309,7 +309,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, cur // Overflow text := []rune(*item.text) - offsets := item.ColorOffsets(col2, bold, current) + offsets := item.colorOffsets(col2, bold, current) maxWidth := C.MaxX() - 3 fullWidth := displayWidth(text) if fullWidth > maxWidth { diff --git a/src/util/eventbox.go b/src/util/eventbox.go index e16b736..ccdbb99 100644 --- a/src/util/eventbox.go +++ b/src/util/eventbox.go @@ -79,6 +79,7 @@ func (b *EventBox) Unwatch(events ...EventType) { } } +// WaitFor blocks the execution until the event is received func (b *EventBox) WaitFor(event EventType) { looping := true for looping { From 9ffcd26d5072f2d9594a4d23034d841918ebcc5d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Mar 2015 16:21:52 +0900 Subject: [PATCH 139/408] Update CHANGELOG - 0.9.5 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9e8e77..cb00422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,30 @@ CHANGELOG ========= +0.9.5 +----- + +### New features + +#### Added `--ansi` option (#150) + +If you give `--ansi` option to fzf, fzf will interpret ANSI color codes from +the input, display the item with the ANSI colors (true colors are not +supported), and strips the codes from the output. This option is off by +default as it entails some overhead. + +### Improvements + +#### Reduced initial memory footprint (#151) + +By removing unnecessary copy of pointers, fzf will use significantly smaller +amount of memory when it's started. The difference is hugely noticeable when +the input is extremely large. (e.g. `locate / | fzf`) + +### Bug fixes + +- Fixed panic on `--no-sort --filter ''` (#149) + 0.9.4 ----- From 618706a5f5b56b24d08857af075877135baa1ea6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Mar 2015 17:19:30 +0900 Subject: [PATCH 140/408] Fix ANSI output in the presence of multibyte characters tree -C | fzf --ansi --tac --- src/ansi.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index fbfa050..42c6916 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -5,6 +5,7 @@ import ( "regexp" "strconv" "strings" + "unicode/utf8" ) type ansiOffset struct { @@ -44,7 +45,6 @@ func extractColor(str *string) (*string, []ansiOffset) { idx := 0 for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) { output.WriteString((*str)[idx:offset[0]]) - newLen := int32(output.Len()) newState := interpretCode((*str)[offset[0]:offset[1]], state) if !newState.equals(state) { @@ -56,6 +56,7 @@ func extractColor(str *string) (*string, []ansiOffset) { if newState.colored() { // Append new offset state = newState + newLen := int32(utf8.RuneCount(output.Bytes())) offsets = append(offsets, ansiOffset{[2]int32{newLen, newLen}, *state}) } else { // Discard state @@ -71,7 +72,7 @@ func extractColor(str *string) (*string, []ansiOffset) { output.WriteString(rest) if state != nil { // Update last offset - (&offsets[len(offsets)-1]).offset[1] = int32(output.Len()) + (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes())) } } outputStr := output.String() From f8c49effd4b285d42cb1740a289cb08008c08181 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Mar 2015 17:43:28 +0900 Subject: [PATCH 141/408] Respect "boldness" of input string --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index bf64ec7..5570f8d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -353,7 +353,7 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, cur if b < e { substr, prefixWidth = processTabs(text[b:e], prefixWidth) - C.CPrint(offset.color, bold, substr) + C.CPrint(offset.color, offset.bold, substr) } index = e From eae53576bd31b6583bb13efc80418d4129e1b813 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Mar 2015 21:25:46 +0900 Subject: [PATCH 142/408] Update --help message --- src/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/options.go b/src/options.go index 573ce3d..73c9e18 100644 --- a/src/options.go +++ b/src/options.go @@ -29,7 +29,7 @@ const usage = `usage: fzf [options] Interface -m, --multi Enable multi-select with tab/shift-tab - --ansi Interpret ANSI color codes and remove from output + --ansi Enable processing of ANSI color codes --no-mouse Disable mouse +c, --no-color Disable colors +2, --no-256 Disable 256-color From 9d138173be9845fc7f2cbeab58b41f27a1f8043f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 23 Mar 2015 01:24:31 +0900 Subject: [PATCH 143/408] Fix #155 - Empty ANSI color code to reset color state --- src/ansi.go | 3 +++ src/ansi_test.go | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/src/ansi.go b/src/ansi.go index 42c6916..f4c8f5a 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -99,6 +99,9 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { } ansiCode = ansiCode[2 : len(ansiCode)-1] + if len(ansiCode) == 0 { + init() + } for _, code := range strings.Split(ansiCode, ";") { if num, err := strconv.Atoi(code); err == nil { switch state256 { diff --git a/src/ansi_test.go b/src/ansi_test.go index d5e0e43..be982eb 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -45,6 +45,14 @@ func TestExtractColor(t *testing.T) { assert(offsets[0], 0, 11, -1, -1, true) }) + src = "\x1b[1mhello \x1b[mworld" + check(func(offsets []ansiOffset) { + if len(offsets) != 1 { + t.Fail() + } + assert(offsets[0], 0, 6, -1, -1, true) + }) + src = "hello \x1b[34;45;1mworld" check(func(offsets []ansiOffset) { if len(offsets) != 1 { From 87447ddd6d0803bf5f6430a62c118b2c519c6a39 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 26 Mar 2015 03:06:31 +0900 Subject: [PATCH 144/408] Add man page (#157) --- install | 4 ++ man/man1/fzf.1 | 186 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 man/man1/fzf.1 diff --git a/install b/install index 50f0076..b819f98 100755 --- a/install +++ b/install @@ -184,6 +184,10 @@ if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then export PATH="$fzf_base/bin:\$PATH" fi +# Man path +# -------- +export MANPATH="\$MANPATH:$fzf_base/man" + # Auto-completion # --------------- $fzf_completion diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 new file mode 100644 index 0000000..151f9a9 --- /dev/null +++ b/man/man1/fzf.1 @@ -0,0 +1,186 @@ +.ig +The MIT License (MIT) + +Copyright (c) 2015 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +.. +.TH fzf 1 "March 2015" "fzf 0.9.5" "fzf - a command-line fuzzy finder" + +.SH NAME +fzf - a command-line fuzzy finder + +.SH SYNOPSIS +fzf [options] + +.SH DESCRIPTION +fzf is a general-purpose command-line fuzzy finder. + +.SH OPTIONS +.SS Search mode +.TP +.B "-x, --extended" +Extended-search mode +.TP +.B "-e, --extended-exact" +Extended-search mode (exact match) +.TP +.B "-i" +Case-insensitive match (default: smart-case match) +.TP +.B "+i" +Case-sensitive match +.TP +.BI "-n, --nth=" "N[,..]" +Comma-separated list of field index expressions for limiting search scope. +See \fBFIELD INDEX EXPRESSION\fR for details. +.TP +.BI "--with-nth=" "N[,..]" +Transform the item using the list of index expressions for search +.TP +.BI "-d, --delimiter=" "STR" +Field delimiter regex for \fI--nth\fR and \fI--with-nth\fR (default: AWK-style) +.SS Search result +.TP +.B "+s, --no-sort" +Do not sort the result +.TP +.B "--tac" +Reverse the order of the input +.RS +e.g. \fBhistory | fzf --tac --no-sort\fR +.RE +.SS Interface +.TP +.B "-m, --multi" +Enable multi-select with tab/shift-tab +.TP +.B "--ansi" +Enable processing of ANSI color codes +.TP +.B "--no-mouse" +Disable mouse +.TP +.B "+c, --no-color" +Disable colors +.TP +.B "+2, --no-256" +Disable 256-color +.TP +.B "--black" +Use black background +.TP +.B "--reverse" +Reverse orientation +.TP +.BI "--prompt=" "STR" +Input prompt (default: '> ') +.SS Scripting +.TP +.BI "-q, --query=" "STR" +Start the finder with the given query +.TP +.B "-1, --select-1" +Automatically select the only match +.TP +.B "-0, --exit-0" +Exit immediately when there's no match +.TP +.BI "-f, --filter=" "STR" +Filter mode. Do not start interactive finder. +.TP +.B "--print-query" +Print query as the first line +.TP +.B "--sync" +Synchronous search for multi-staged filtering +.RS +e.g. \fBfzf --multi | fzf --sync\fR +.RE + +.SH ENVIRONMENT +.TP +.B FZF_DEFAULT_COMMAND +Default command to use when input is tty +.TP +.B FZF_DEFAULT_OPTS +Default options. e.g. \fB--extended --ansi\fR + +.SH EXIT STATUS +.BR 0 " Normal exit" +.br +.BR 1 " Interrupted with \fBCTRL-C\fR or \fBESC\fR" + +.SH FIELD INDEX EXPRESSION + +A field index expression can be a non-zero integer or a range expression +([BEGIN]..[END]). \fI--nth\fR and \fI--with-nth\fR take a comma-separated list +of field index expressions. + +.SS Examples +.BR 1 " The 1st field" +.br +.BR 2 " The 2nd field" +.br +.BR -1 " The last field" +.br +.BR -2 " The 2nd to last field" +.br +.BR 3..5 " From the 3rd field to the 5th field" +.br +.BR 2.. " From the 2nd field to the last field" +.br +.BR ..-3 " From the 1st field to the 3rd to the last field" +.br +.BR .. " All the fields" +.br + +.SH EXTENDED SEARCH MODE + +With \fI-x\fR or \fI--extended\fR option, fzf will start in "extended-search +mode". In this mode, you can specify multiple patterns delimited by spaces, +such as: \fB^music .mp3$ sbtrkt !rmx\fR + +.SS Exact-match (quoted) +A term that is prefixed by a single-quote character (') is interpreted as an +"exact-match" (or "non-fuzzy") term. fzf will search for the exact occurrence +of the string. + +.SS Anchored-match +A term can be prefixed by ^, or suffixed by $ to become an anchored-match term +and used to search items that starts with or ends with the given string. +An anchored-match term is also an exact-match term. + +.SS Negation +If a term is prefixed by !, fzf will exclude the items that matches the term +from the result. + +.SS Extended-exact mode +If you don't need fuzzy matching at all and do not wish to "quote" every word, +start fzf with \fI-e\fR or \fI--extended-exact\fR option (instead of \fI-x\fR or +\fI--extended\fR). + +.SH AUTHOR +Junegunn Choi (junegunn.c@gmail.com) + +.SH SEE ALSO +https://github.com/junegunn/fzf + +.SH LICENSE +MIT From eb8fef0031a4d365093617238dac05d6f6c16a1a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 26 Mar 2015 03:11:08 +0900 Subject: [PATCH 145/408] Add man path only when the directory exists $fzf_base/man may not exist when installed with Homebrew. --- install | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install b/install index b819f98..ce001cb 100755 --- a/install +++ b/install @@ -186,7 +186,9 @@ fi # Man path # -------- -export MANPATH="\$MANPATH:$fzf_base/man" +if [ -d "$fzf_base/man" ]; then + export MANPATH="\$MANPATH:$fzf_base/man" +fi # Auto-completion # --------------- From 91876e98cd361c627664502f9da5e34f726d7f73 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 26 Mar 2015 03:26:28 +0900 Subject: [PATCH 146/408] Avoid duplicate paths in MANPATH --- install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install b/install index ce001cb..55fb2e5 100755 --- a/install +++ b/install @@ -186,7 +186,7 @@ fi # Man path # -------- -if [ -d "$fzf_base/man" ]; then +if [[ ! "\$MANPATH" =~ "$fzf_base/man" && -d "$fzf_base/man" ]]; then export MANPATH="\$MANPATH:$fzf_base/man" fi From fdaa4e9b18a761dd0c60a1d4429c9f6c3eb58c96 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 26 Mar 2015 03:44:18 +0900 Subject: [PATCH 147/408] Append (not prepend) bin directory to PATH Prepending can be problematic when the user install fzf using Homebrew, execute the install script, and later upgrade fzf with Homebrew, and do not rerun the install script. In that case, even though the homebrew package is upgraded, the older version will still be used. --- install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install b/install index 55fb2e5..8f9d355 100755 --- a/install +++ b/install @@ -181,7 +181,7 @@ for shell in bash zsh; do # Setup fzf # --------- if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then - export PATH="$fzf_base/bin:\$PATH" + export PATH="\$PATH:$fzf_base/bin" fi # Man path From 39f43587d027be6e417a7b162b9d2b4fadc5959e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 26 Mar 2015 10:28:14 +0900 Subject: [PATCH 148/408] Fix typo in man page --- man/man1/fzf.1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 151f9a9..5ef9c6a 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -155,26 +155,26 @@ of field index expressions. With \fI-x\fR or \fI--extended\fR option, fzf will start in "extended-search mode". In this mode, you can specify multiple patterns delimited by spaces, -such as: \fB^music .mp3$ sbtrkt !rmx\fR +such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR .SS Exact-match (quoted) A term that is prefixed by a single-quote character (') is interpreted as an -"exact-match" (or "non-fuzzy") term. fzf will search for the exact occurrence +"exact-match" (or "non-fuzzy") term. fzf will search for the exact occurrences of the string. .SS Anchored-match -A term can be prefixed by ^, or suffixed by $ to become an anchored-match term -and used to search items that starts with or ends with the given string. -An anchored-match term is also an exact-match term. +A term can be prefixed by ^, or suffixed by $ to become an anchored-match term. +Then fzf will search for the items that start with or end with the given +string. An anchored-match term is also an exact-match term. .SS Negation If a term is prefixed by !, fzf will exclude the items that matches the term from the result. .SS Extended-exact mode -If you don't need fuzzy matching at all and do not wish to "quote" every word, -start fzf with \fI-e\fR or \fI--extended-exact\fR option (instead of \fI-x\fR or -\fI--extended\fR). +If you don't need fuzzy matching at all and do not wish to "quote" (prefixing +with ') every word, start fzf with \fI-e\fR or \fI--extended-exact\fR option +(instead of \fI-x\fR or \fI--extended\fR). .SH AUTHOR Junegunn Choi (junegunn.c@gmail.com) From c4bf820dc3133d84386e63430bebfaa8045509f2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 26 Mar 2015 10:31:11 +0900 Subject: [PATCH 149/408] Update man page --- man/man1/fzf.1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 5ef9c6a..bfb358f 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -168,7 +168,7 @@ Then fzf will search for the items that start with or end with the given string. An anchored-match term is also an exact-match term. .SS Negation -If a term is prefixed by !, fzf will exclude the items that matches the term +If a term is prefixed by !, fzf will exclude the items that satisfy the term from the result. .SS Extended-exact mode @@ -177,10 +177,10 @@ with ') every word, start fzf with \fI-e\fR or \fI--extended-exact\fR option (instead of \fI-x\fR or \fI--extended\fR). .SH AUTHOR -Junegunn Choi (junegunn.c@gmail.com) +Junegunn Choi (\fIjunegunn.c@gmail.com\fR) .SH SEE ALSO -https://github.com/junegunn/fzf +.I https://github.com/junegunn/fzf .SH LICENSE MIT From 4ad92e3a0bf0c48222cd22c6b9e9cfbced266582 Mon Sep 17 00:00:00 2001 From: Michael Rapadas Date: Thu, 26 Mar 2015 13:28:19 -0700 Subject: [PATCH 150/408] Updates MacVim wiki link in README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 65d5d44..cb18e4c 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,8 @@ let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s' If you're running MacVim on OSX, I recommend you to use iTerm2 as the launcher. Refer to the [this wiki -page](https://github.com/junegunn/fzf/wiki/fzf-with-MacVim-and-iTerm2) to see +page](https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2) to see + how to set up. #### `fzf#run([options])` From ce7d4a1c53ef73ad4f02726f6ccf1df1d6a3eb1c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 27 Mar 2015 12:35:06 +0900 Subject: [PATCH 151/408] Fix #162 - Ignore \e[K --- src/Makefile | 2 +- src/ansi.go | 5 ++++- src/ansi_test.go | 8 ++++++++ src/constants.go | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Makefile b/src/Makefile index 2604c3b..d833ce9 100644 --- a/src/Makefile +++ b/src/Makefile @@ -18,7 +18,7 @@ BINDIR := ../bin BINARY32 := fzf-$(GOOS)_386 BINARY64 := fzf-$(GOOS)_amd64 -VERSION = $(shell fzf/$(BINARY64) --version) +VERSION = $(shell fzf/$(BINARY64) --version | sed 's/-p[0-9]*//') RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 diff --git a/src/ansi.go b/src/ansi.go index f4c8f5a..126a992 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -33,7 +33,7 @@ func (s *ansiState) equals(t *ansiState) bool { var ansiRegex *regexp.Regexp func init() { - ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*m") + ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") } func extractColor(str *string) (*string, []ansiOffset) { @@ -87,6 +87,9 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { } else { state = &ansiState{prevState.fg, prevState.bg, prevState.bold} } + if ansiCode[len(ansiCode)-1] == 'K' { + return state + } ptr := &state.fg state256 := 0 diff --git a/src/ansi_test.go b/src/ansi_test.go index be982eb..9f62840 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -53,6 +53,14 @@ func TestExtractColor(t *testing.T) { assert(offsets[0], 0, 6, -1, -1, true) }) + src = "\x1b[1mhello \x1b[Kworld" + check(func(offsets []ansiOffset) { + if len(offsets) != 1 { + t.Fail() + } + assert(offsets[0], 0, 11, -1, -1, true) + }) + src = "hello \x1b[34;45;1mworld" check(func(offsets []ansiOffset) { if len(offsets) != 1 { diff --git a/src/constants.go b/src/constants.go index 729bc5c..394a40d 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.5" +const Version = "0.9.5-p1" // fzf events const ( From f9d6b83f5e895a1557e32d780cc689067e8d05ac Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 27 Mar 2015 12:47:42 +0900 Subject: [PATCH 152/408] Drop patch number and stick with M.m.p convention --- src/Makefile | 2 +- src/constants.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Makefile b/src/Makefile index d833ce9..2604c3b 100644 --- a/src/Makefile +++ b/src/Makefile @@ -18,7 +18,7 @@ BINDIR := ../bin BINARY32 := fzf-$(GOOS)_386 BINARY64 := fzf-$(GOOS)_amd64 -VERSION = $(shell fzf/$(BINARY64) --version | sed 's/-p[0-9]*//') +VERSION = $(shell fzf/$(BINARY64) --version) RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 diff --git a/src/constants.go b/src/constants.go index 394a40d..006a1bd 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.5-p1" +const Version = "0.9.6" // fzf events const ( From 9cfecf7f0bb52441c27b769427fdf05f45b3110d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 28 Mar 2015 21:37:37 +0900 Subject: [PATCH 153/408] Fix test failure --- test/test_go.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_go.rb b/test/test_go.rb index 299fefa..6f67da7 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -4,6 +4,8 @@ require 'minitest/autorun' require 'fileutils' +Dir.chdir File.expand_path('../../', __FILE__) + class NilClass def include? str false From 2a167aa030b244060fc479d2b88fdb9b9171d026 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 29 Mar 2015 02:59:32 +0900 Subject: [PATCH 154/408] Implement --expect option to support simple key bindings (#163) --- CHANGELOG.md | 24 ++++++++++++++++++++++++ install | 2 +- man/man1/fzf.1 | 11 +++++++++++ src/curses/curses.go | 27 ++++++++++++++++++++++++--- src/options.go | 33 +++++++++++++++++++++++++++++++++ src/options_test.go | 25 ++++++++++++++++++++++++- src/terminal.go | 24 ++++++++++++++++++++++++ src/util/util.go | 4 ++++ test/test_go.rb | 27 +++++++++++++++++++++++++++ 9 files changed, 172 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb00422..ef2c957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,30 @@ CHANGELOG ========= +0.9.6 +----- + +### New features + +#### Added `--expect` option (#163) + +If you provide a comma-separated list of keys with `--expect` option, fzf will +allow you to select the match and complete the finder when any of the keys is +pressed. Additionally, fzf will print the name of the key pressed as the first +line of the output so that your script can decide what to do next based on the +information. + +```sh +fzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@ +``` + +The updated vim plugin uses this option to implement +[ctrlp](https://github.com/kien/ctrlp.vim)-compatible key bindings. + +### Bug fixes + +- Fixed to ignore ANSI escape code `\e[K` (#162) + 0.9.5 ----- diff --git a/install b/install index 8f9d355..eb85a8b 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.5 +version=0.9.6 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index bfb358f..c6bf054 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -108,6 +108,17 @@ Filter mode. Do not start interactive finder. .B "--print-query" Print query as the first line .TP +.BI "--expect=" "KEY[,..]" +Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, +or a single character) that can be used to complete fzf in addition to the +default enter key. When this option is set, fzf will print the name of the key +pressed as the first line of its output (or as the second line if +\fB--print-query\fR is also used). The line will be empty if fzf is completed +with the default enter key. +.RS +e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR +.RE +.TP .B "--sync" Synchronous search for multi-staged filtering .RS diff --git a/src/curses/curses.go b/src/curses/curses.go index dfd7cf5..d6aafd7 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -61,10 +61,20 @@ const ( PgUp PgDn - AltB - AltF - AltD + F1 + F2 + F3 + F4 + AltBS + AltA + AltB + AltC + AltD + AltE + AltF + + AltZ = AltA + 'z' - 'a' ) // Pallete @@ -324,6 +334,14 @@ func escSequence(sz *int) Event { return Event{CtrlE, 0, nil} case 77: return mouseSequence(sz) + case 80: + return Event{F1, 0, nil} + case 81: + return Event{F2, 0, nil} + case 82: + return Event{F3, 0, nil} + case 83: + return Event{F4, 0, nil} case 49, 50, 51, 52, 53, 54: if len(_buf) < 4 { return Event{Invalid, 0, nil} @@ -369,6 +387,9 @@ func escSequence(sz *int) Event { } // _buf[2] } // _buf[2] } // _buf[1] + if _buf[1] >= 'a' && _buf[1] <= 'z' { + return Event{AltA + int(_buf[1]) - 'a', 0, nil} + } return Event{Invalid, 0, nil} } diff --git a/src/options.go b/src/options.go index 73c9e18..89b1c36 100644 --- a/src/options.go +++ b/src/options.go @@ -5,6 +5,9 @@ import ( "os" "regexp" "strings" + "unicode/utf8" + + "github.com/junegunn/fzf/src/curses" "github.com/junegunn/go-shellwords" ) @@ -43,6 +46,7 @@ const usage = `usage: fzf [options] -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 + --expect=KEYS Comma-separated list of keys to complete fzf --sync Synchronous search for multi-staged filtering (e.g. 'fzf --multi | fzf --sync') @@ -93,6 +97,7 @@ type Options struct { Select1 bool Exit0 bool Filter *string + Expect []int PrintQuery bool Sync bool Version bool @@ -119,6 +124,7 @@ func defaultOptions() *Options { Select1: false, Exit0: false, Filter: nil, + Expect: []int{}, PrintQuery: false, Sync: false, Version: false} @@ -191,6 +197,29 @@ func delimiterRegexp(str string) *regexp.Regexp { return rx } +func isAlphabet(char uint8) bool { + return char >= 'a' && char <= 'z' +} + +func parseKeyChords(str string) []int { + var chords []int + for _, key := range strings.Split(str, ",") { + lkey := strings.ToLower(key) + if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { + chords = append(chords, curses.CtrlA+int(lkey[5])-'a') + } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { + chords = append(chords, curses.AltA+int(lkey[4])-'a') + } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' { + chords = append(chords, curses.F1+int(key[1])-'1') + } else if utf8.RuneCountInString(key) == 1 { + chords = append(chords, curses.AltZ+int([]rune(key)[0])) + } else { + errorExit("unsupported key: " + key) + } + } + return chords +} + func parseOptions(opts *Options, allArgs []string) { for i := 0; i < len(allArgs); i++ { arg := allArgs[i] @@ -208,6 +237,8 @@ func parseOptions(opts *Options, allArgs []string) { case "-f", "--filter": filter := nextString(allArgs, &i, "query string required") opts.Filter = &filter + case "--expect": + opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required")) case "-d", "--delimiter": opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) case "-n", "--nth": @@ -285,6 +316,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.WithNth = splitNth(value) } else if match, _ := optString(arg, "-s|--sort="); match { opts.Sort = 1 // Don't care + } else if match, value := optString(arg, "--expect="); match { + opts.Expect = parseKeyChords(value) } else { errorExit("unknown option: " + arg) } diff --git a/src/options_test.go b/src/options_test.go index 782ad79..b20cd6a 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -1,6 +1,10 @@ package fzf -import "testing" +import ( + "testing" + + "github.com/junegunn/fzf/src/curses" +) func TestDelimiterRegex(t *testing.T) { rx := delimiterRegexp("*") @@ -65,3 +69,22 @@ func TestIrrelevantNth(t *testing.T) { } } } + +func TestExpectKeys(t *testing.T) { + keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g") + check := func(key int, expected int) { + if key != expected { + t.Errorf("%d != %d", key, expected) + } + } + check(len(keys), 9) + check(keys[0], curses.CtrlZ) + check(keys[1], curses.AltZ) + check(keys[2], curses.F2) + check(keys[3], curses.AltZ+'@') + check(keys[4], curses.AltA) + check(keys[5], curses.AltZ+'!') + check(keys[6], curses.CtrlA+'g'-'a') + check(keys[7], curses.AltZ+'J') + check(keys[8], curses.AltZ+'g') +} diff --git a/src/terminal.go b/src/terminal.go index 5570f8d..2d191a9 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -28,6 +28,8 @@ type Terminal struct { yanked []rune input []rune multi bool + expect []int + pressed int printQuery bool count int progress int @@ -91,6 +93,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { yanked: []rune{}, input: input, multi: opts.Multi, + expect: opts.Expect, + pressed: 0, printQuery: opts.PrintQuery, merger: EmptyMerger, selected: make(map[*string]selectedItem), @@ -150,6 +154,19 @@ func (t *Terminal) output() { if t.printQuery { fmt.Println(string(t.input)) } + if len(t.expect) > 0 { + if t.pressed == 0 { + fmt.Println() + } else if util.Between(t.pressed, C.AltA, C.AltZ) { + fmt.Printf("alt-%c\n", t.pressed+'a'-C.AltA) + } else if util.Between(t.pressed, C.F1, C.F4) { + fmt.Printf("f%c\n", t.pressed+'1'-C.F1) + } else if util.Between(t.pressed, C.CtrlA, C.CtrlZ) { + fmt.Printf("ctrl-%c\n", t.pressed+'a'-C.CtrlA) + } else { + fmt.Printf("%c\n", t.pressed-C.AltZ) + } + } if len(t.selected) == 0 { cnt := t.merger.Length() if cnt > 0 && cnt > t.cy { @@ -535,6 +552,13 @@ func (t *Terminal) Loop() { req(reqInfo) } } + for _, key := range t.expect { + if event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ { + t.pressed = key + req(reqClose) + break + } + } switch event.Type { case C.Invalid: t.mutex.Unlock() diff --git a/src/util/util.go b/src/util/util.go index 1f53cc7..2d680b1 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -61,6 +61,10 @@ func DurWithin( return val } +func Between(val int, min int, max int) bool { + return val >= min && val <= max +} + // IsTty returns true is stdin is a terminal func IsTty() bool { return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 diff --git a/test/test_go.rb b/test/test_go.rb index 6f67da7..adfc0d0 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -425,6 +425,33 @@ class TestGoFZF < TestBase tmux.send_keys :BTab, :BTab, :BTab, :Enter assert_equal %w[1000 900 800], readonce.split($/) end + + def test_expect + test = lambda do |key, feed, expected = key| + tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter + tmux.until { |lines| lines[-2].include? '100/100' } + tmux.send_keys '55' + tmux.send_keys *feed + assert_equal [expected, '55'], readonce.split($/) + end + test.call 'ctrl-t', 'C-T' + test.call 'ctrl-t', 'Enter', '' + test.call 'alt-c', [:Escape, :c] + test.call 'f1', 'f1' + test.call 'f2', 'f2' + test.call 'f3', 'f3' + test.call 'f2,f4', 'f2', 'f2' + test.call 'f2,f4', 'f4', 'f4' + test.call '@', '@' + end + + def test_expect_print_query + tmux.send_keys "seq 1 100 | #{fzf '--expect=alt-z', :print_query}", :Enter + tmux.until { |lines| lines[-2].include? '100/100' } + tmux.send_keys '55' + tmux.send_keys :Escape, :z + assert_equal ['55', 'alt-z', '55'], readonce.split($/) + end end module TestShell From e865144aced734a942fd6876fe0c8402be214659 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 29 Mar 2015 03:00:32 +0900 Subject: [PATCH 155/408] [vim] Implement ctrlp-compatible key bindings (#139, #96, #61) --- README.md | 4 ++++ plugin/fzf.vim | 26 ++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cb18e4c..55a5ea4 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,10 @@ If you have set up fzf for Vim, `:FZF` command will be added. :FZF --no-sort -m /tmp ``` +Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key, +`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window, +in new tabs, in horizontal splits, or in vertical splits respectively. + Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS` also apply here. diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 713dfff..9c8ae62 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -26,6 +26,7 @@ let s:launcher = 'xterm -e bash -ic %s' let s:fzf_go = expand(':h:h').'/bin/fzf' let s:fzf_rb = expand(':h:h').'/fzf' let s:fzf_tmux = expand(':h:h').'/bin/fzf-tmux' +let s:legacy = 0 let s:cpo_save = &cpo set cpo&vim @@ -40,6 +41,7 @@ function! s:fzf_exec() let s:exec = path[0] elseif executable(s:fzf_rb) let s:exec = s:fzf_rb + let s:legacy = 1 else call system('type fzf') if v:shell_error @@ -221,7 +223,7 @@ function! s:callback(dict, temps) if type(a:dict.sink) == 2 call a:dict.sink(line) else - execute a:dict.sink.' '.s:escape(line) + execute a:dict.sink s:escape(line) endif endfor endif @@ -238,6 +240,9 @@ endfunction function! s:cmd(bang, ...) abort let args = copy(a:000) + if !s:legacy + let args = add(args, '--expect=ctrl-t,ctrl-x,ctrl-v') + endif let opts = {} if len(args) > 0 && isdirectory(expand(args[-1])) let opts.dir = remove(args, -1) @@ -245,7 +250,24 @@ function! s:cmd(bang, ...) abort if !a:bang let opts.down = get(g:, 'fzf_tmux_height', s:default_tmux_height) endif - call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts)) + + if s:legacy + call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts)) + else + let output = fzf#run(extend({ 'options': join(args) }, opts)) + if empty(output) + return + endif + let key = remove(output, 0) + if key == 'ctrl-t' | let cmd = 'tabedit' + elseif key == 'ctrl-x' | let cmd = 'split' + elseif key == 'ctrl-v' | let cmd = 'vsplit' + else | let cmd = 'e' + endif + for item in output + execute cmd s:escape(item) + endfor + endif endfunction command! -nargs=* -complete=dir -bang FZF call s:cmd('' == '!', ) From d988f3fa50e0cefc8f0f1e69c296ecc9015b485c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 29 Mar 2015 03:12:55 +0900 Subject: [PATCH 156/408] Retain ANSI background color --- src/item.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/item.go b/src/item.go index c94166f..9e2e1e7 100644 --- a/src/item.go +++ b/src/item.go @@ -118,7 +118,7 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset } else { ansi := item.colors[curr-1] bg := ansi.color.bg - if current { + if current && bg == -1 { bg = int(curses.DarkBG) } offsets = append(offsets, colorOffset{ From 0a3d3460b134ada329bf0d1e209c736baf4b08c4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 29 Mar 2015 04:08:37 +0900 Subject: [PATCH 157/408] Update man page --- man/man1/fzf.1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index c6bf054..25c3f8c 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -103,7 +103,8 @@ Automatically select the only match Exit immediately when there's no match .TP .BI "-f, --filter=" "STR" -Filter mode. Do not start interactive finder. +Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR, +fzf becomes a fuzzy-version of grep. .TP .B "--print-query" Print query as the first line @@ -120,7 +121,8 @@ e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR .RE .TP .B "--sync" -Synchronous search for multi-staged filtering +Synchronous search for multi-staged filtering. If specified, fzf will launch +ncurses finder only after the input stream is complete. .RS e.g. \fBfzf --multi | fzf --sync\fR .RE From cef93f700bb4be4c5c33a8ceca3bf0f5ccf31620 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 29 Mar 2015 04:09:45 +0900 Subject: [PATCH 158/408] 0.9.6 --- man/man1/fzf.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 25c3f8c..8c63b3f 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "March 2015" "fzf 0.9.5" "fzf - a command-line fuzzy finder" +.TH fzf 1 "March 2015" "fzf 0.9.6" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder From 8a4db3c0041c35ca195468c779d42829d7dabe59 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 29 Mar 2015 11:14:16 +0900 Subject: [PATCH 159/408] [vim] Fix #167 - :FZF with directory --- plugin/fzf.vim | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 9c8ae62..94df15f 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -241,7 +241,7 @@ endfunction function! s:cmd(bang, ...) abort let args = copy(a:000) if !s:legacy - let args = add(args, '--expect=ctrl-t,ctrl-x,ctrl-v') + let args = insert(args, '--expect=ctrl-t,ctrl-x,ctrl-v', 0) endif let opts = {} if len(args) > 0 && isdirectory(expand(args[-1])) @@ -264,9 +264,14 @@ function! s:cmd(bang, ...) abort elseif key == 'ctrl-v' | let cmd = 'vsplit' else | let cmd = 'e' endif - for item in output - execute cmd s:escape(item) - endfor + try + call s:pushd(opts) + for item in output + execute cmd s:escape(item) + endfor + finally + call s:popd(opts) + endtry endif endfunction From 1c65139888f7dd971d93a2c1210cd634e1ef12f4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 30 Mar 2015 10:15:05 +0900 Subject: [PATCH 160/408] Update git ls-tree example (close #168) --- README.md | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 55a5ea4..d5e58ce 100644 --- a/README.md +++ b/README.md @@ -423,18 +423,9 @@ If you're running fzf in a large git repository, `git ls-tree` can boost up the speed of the traversal. ```sh -# Copy the original fzf function to __fzf -declare -f __fzf > /dev/null || - eval "$(echo "__fzf() {"; declare -f fzf | \grep -v '^{' | tail -n +2)" - -# Use git ls-tree when possible -fzf() { - if [ -n "$(git rev-parse HEAD 2> /dev/null)" ]; then - FZF_DEFAULT_COMMAND="git ls-tree -r --name-only HEAD" __fzf "$@" - else - __fzf "$@" - fi -} +export FZF_DEFAULT_COMMAND=' + (git ls-tree -r --name-only HEAD || + find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null' ``` #### Using fzf with tmux panes From 39d7177bd3280ab94e7cf268f91652f580e8ca19 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 31 Mar 2015 15:40:33 +0900 Subject: [PATCH 161/408] [ruby] Stub out --expect option So that it can be used with the recent Vim plugin although extra key bindings are not available --- fzf | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fzf b/fzf index 28518ae..0cdb115 100755 --- a/fzf +++ b/fzf @@ -126,6 +126,7 @@ class FZF @reverse = false @prompt = '> ' @shr_mtx = Mutex.new + @expect = false @print_query = false argv = @@ -200,6 +201,11 @@ class FZF when '--no-print-query' then @print_query = false when '-e', '--extended-exact' then @extended = :exact when '+e', '--no-extended-exact' then @extended = nil + when '--expect' + argv.shift + @expect = true + when /^--expect=(.*)$/ + @expect = true when '--tac', '--sync' # XXX else @@ -276,10 +282,12 @@ class FZF if loaded if @select1 && len == 1 puts @query if @print_query + puts if @expect burp(empty ? matches.first : matches.first.first) exit 0 elsif @exit0 && len == 0 puts @query if @print_query + puts if @expect exit 0 end end @@ -1153,6 +1161,7 @@ class FZF C.close_screen q, selects = geta(:@query, :@selects) @stdout.puts q if @print_query + @stdout.puts if @expect if got if selects.empty? burp got From 84a7499ae357bc3b3a82890d2e44d9c300af0c13 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 31 Mar 2015 20:52:16 +0900 Subject: [PATCH 162/408] Fix #172 - Print empty line when fzf with expect finished by -1 or -0 --- src/core.go | 3 +++ test/test_go.rb | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/core.go b/src/core.go index ebb7d66..204532b 100644 --- a/src/core.go +++ b/src/core.go @@ -216,6 +216,9 @@ func Run(options *Options) { if opts.PrintQuery { fmt.Println(opts.Query) } + if len(opts.Expect) > 0 { + fmt.Println() + } for i := 0; i < count; i++ { fmt.Println(val.Get(i).AsString()) } diff --git a/test/test_go.rb b/test/test_go.rb index adfc0d0..a47e422 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -452,6 +452,11 @@ class TestGoFZF < TestBase tmux.send_keys :Escape, :z assert_equal ['55', 'alt-z', '55'], readonce.split($/) end + + def test_expect_print_query_select_1 + tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter + assert_equal ['55', '', '55'], readonce.split($/) + end end module TestShell From 50292adacbad70f9561bc1e22ccbd3adea22481a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 31 Mar 2015 22:05:02 +0900 Subject: [PATCH 163/408] Implement --toggle-sort option (#173) --- CHANGELOG.md | 14 +++++++++++++ README.md | 1 + fzf | 4 +++- install | 2 +- man/man1/fzf.1 | 6 +++++- shell/key-bindings.bash | 2 +- shell/key-bindings.fish | 2 +- shell/key-bindings.zsh | 2 +- src/constants.go | 2 +- src/core.go | 14 ++++++++----- src/matcher.go | 11 ++++++++-- src/options.go | 35 ++++++++++++++++++++++++++++---- src/options_test.go | 45 +++++++++++++++++++++++++++++++++++++++-- src/pattern.go | 12 +++++++---- src/terminal.go | 17 ++++++++++++++-- test/test_go.rb | 13 ++++++++++++ 16 files changed, 156 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef2c957..d3b1cf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ CHANGELOG ========= +0.9.7 +----- + +### New features + +- Added `--toggle-sort` option (#173) + - `--toggle-sort=ctrl-r` is applied to `CTRL-R` shell extension + +### Bug fixes + +- Fixed to print empty line if `--expect` is set and fzf is completed by + `--select-1` or `--exit-0` (#172) +- Fixed to allow comma character as an argument to `--expect` option + 0.9.6 ----- diff --git a/README.md b/README.md index d5e58ce..6070f9a 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ fish. - `CTRL-T` - Paste the selected file path(s) into the command line - `CTRL-R` - Paste the selected command from history into the command line + - Sort is disabled by default. Press `CTRL-R` again to toggle sort - `ALT-C` - cd into the selected directory If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You diff --git a/fzf b/fzf index 0cdb115..69bf14f 100755 --- a/fzf +++ b/fzf @@ -206,7 +206,9 @@ class FZF @expect = true when /^--expect=(.*)$/ @expect = true - when '--tac', '--sync' + when '--toggle-sort' + argv.shift + when '--tac', '--sync', '--toggle-sort', /^--toggle-sort=(.*)$/ # XXX else usage 1, "illegal option: #{o}" diff --git a/install b/install index eb85a8b..a8bb074 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.6 +version=0.9.7 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 8c63b3f..d317adb 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -111,7 +111,7 @@ Print query as the first line .TP .BI "--expect=" "KEY[,..]" Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, -or a single character) that can be used to complete fzf in addition to the +or any single character) that can be used to complete fzf in addition to the default enter key. When this option is set, fzf will print the name of the key pressed as the first line of its output (or as the second line if \fB--print-query\fR is also used). The line will be empty if fzf is completed @@ -120,6 +120,10 @@ with the default enter key. e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR .RE .TP +.BI "--toggle-sort=" "KEY" +Key to toggle sort (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, +or any single character) +.TP .B "--sync" Synchronous search for multi-staged filtering. If specified, fzf will launch ncurses finder only after the input stream is complete. diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 9011247..d7f0903 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -44,7 +44,7 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then fi # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"' + bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. --toggle-sort=ctrl-r | sed \"s/ *[0-9]* *//\")\e\C-e\er"' # ALT-C - cd into the selected directory bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index ce1eea7..6e9efa4 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -44,7 +44,7 @@ function fzf_key_bindings end function __fzf_ctrl_r - history | fzf +s +m > $TMPDIR/fzf.result + history | fzf +s +m --toggle-sort=ctrl-r > $TMPDIR/fzf.result and commandline (cat $TMPDIR/fzf.result) commandline -f repaint rm -f $TMPDIR/fzf.result diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 6eb8083..4780658 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -45,7 +45,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected - if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. -q "$LBUFFER"); then + if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. --toggle-sort=ctrl-r -q "$LBUFFER"); then num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') LBUFFER=!$num zle expand-history diff --git a/src/constants.go b/src/constants.go index 006a1bd..5cd6d80 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.6" +const Version = "0.9.7" // fzf events const ( diff --git a/src/core.go b/src/core.go index 204532b..9f33b41 100644 --- a/src/core.go +++ b/src/core.go @@ -44,7 +44,7 @@ func initProcs() { /* Reader -> EvtReadFin Reader -> EvtReadNew -> Matcher (restart) -Terminal -> EvtSearchNew -> Matcher (restart) +Terminal -> EvtSearchNew:bool -> Matcher (restart) Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchFin -> Terminal (update list) */ @@ -54,6 +54,7 @@ func Run(options *Options) { initProcs() opts := ParseOptions() + sort := opts.Sort > 0 if opts.Version { fmt.Println(Version) @@ -112,7 +113,7 @@ func Run(options *Options) { } // Reader - streamingFilter := opts.Filter != nil && opts.Sort == 0 && !opts.Tac && !opts.Sync + streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync if !streamingFilter { reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} go reader.ReadSource() @@ -123,7 +124,7 @@ func Run(options *Options) { return BuildPattern( opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) } - matcher := NewMatcher(patternBuilder, opts.Sort > 0, opts.Tac, eventBox) + matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) // Filtering mode if opts.Filter != nil { @@ -190,11 +191,14 @@ func Run(options *Options) { reading = reading && evt == EvtReadNew snapshot, count := chunkList.Snapshot() terminal.UpdateCount(count, !reading) - matcher.Reset(snapshot, terminal.Input(), false, !reading) + matcher.Reset(snapshot, terminal.Input(), false, !reading, sort) case EvtSearchNew: + if value.(bool) { + sort = !sort + } snapshot, _ := chunkList.Snapshot() - matcher.Reset(snapshot, terminal.Input(), true, !reading) + matcher.Reset(snapshot, terminal.Input(), true, !reading, sort) delay = false case EvtSearchProgress: diff --git a/src/matcher.go b/src/matcher.go index a3a9bd0..0f3b409 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -15,6 +15,7 @@ type MatchRequest struct { chunks []*Chunk pattern *Pattern final bool + sort bool } // Matcher is responsible for performing search @@ -69,6 +70,12 @@ func (m *Matcher) Loop() { events.Clear() }) + if request.sort != m.sort { + m.sort = request.sort + m.mergerCache = make(map[string]*Merger) + clearChunkCache() + } + // Restart search patternString := request.pattern.AsString() var merger *Merger @@ -203,7 +210,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } // Reset is called to interrupt/signal the ongoing search -func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool) { +func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool) { pattern := m.patternBuilder(patternRunes) var event util.EventType @@ -212,5 +219,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final } else { event = reqRetry } - m.reqBox.Set(event, MatchRequest{chunks, pattern, final}) + m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort}) } diff --git a/src/options.go b/src/options.go index 89b1c36..fcf3097 100644 --- a/src/options.go +++ b/src/options.go @@ -47,6 +47,7 @@ const usage = `usage: fzf [options] -f, --filter=STR Filter mode. Do not start interactive finder. --print-query Print query as the first line --expect=KEYS Comma-separated list of keys to complete fzf + --toggle-sort=KEY Key to toggle sort --sync Synchronous search for multi-staged filtering (e.g. 'fzf --multi | fzf --sync') @@ -97,6 +98,7 @@ type Options struct { Select1 bool Exit0 bool Filter *string + ToggleSort int Expect []int PrintQuery bool Sync bool @@ -124,6 +126,7 @@ func defaultOptions() *Options { Select1: false, Exit0: false, Filter: nil, + ToggleSort: 0, Expect: []int{}, PrintQuery: false, Sync: false, @@ -201,9 +204,21 @@ func isAlphabet(char uint8) bool { return char >= 'a' && char <= 'z' } -func parseKeyChords(str string) []int { +func parseKeyChords(str string, message string) []int { + if len(str) == 0 { + errorExit(message) + } + + tokens := strings.Split(str, ",") + if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 { + tokens = append(tokens, ",") + } + var chords []int - for _, key := range strings.Split(str, ",") { + for _, key := range tokens { + if len(key) == 0 { + continue // ignore + } lkey := strings.ToLower(key) if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { chords = append(chords, curses.CtrlA+int(lkey[5])-'a') @@ -220,6 +235,14 @@ func parseKeyChords(str string) []int { return chords } +func checkToggleSort(str string) int { + keys := parseKeyChords(str, "key name required") + if len(keys) != 1 { + errorExit("multiple keys specified") + } + return keys[0] +} + func parseOptions(opts *Options, allArgs []string) { for i := 0; i < len(allArgs); i++ { arg := allArgs[i] @@ -238,7 +261,9 @@ func parseOptions(opts *Options, allArgs []string) { filter := nextString(allArgs, &i, "query string required") opts.Filter = &filter case "--expect": - opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required")) + opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") + case "--toggle-sort": + opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required")) case "-d", "--delimiter": opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) case "-n", "--nth": @@ -316,8 +341,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.WithNth = splitNth(value) } else if match, _ := optString(arg, "-s|--sort="); match { opts.Sort = 1 // Don't care + } else if match, value := optString(arg, "--toggle-sort="); match { + opts.ToggleSort = checkToggleSort(value) } else if match, value := optString(arg, "--expect="); match { - opts.Expect = parseKeyChords(value) + opts.Expect = parseKeyChords(value, "key names required") } else { errorExit("unknown option: " + arg) } diff --git a/src/options_test.go b/src/options_test.go index b20cd6a..36959da 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -70,8 +70,8 @@ func TestIrrelevantNth(t *testing.T) { } } -func TestExpectKeys(t *testing.T) { - keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g") +func TestParseKeys(t *testing.T) { + keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "") check := func(key int, expected int) { if key != expected { t.Errorf("%d != %d", key, expected) @@ -88,3 +88,44 @@ func TestExpectKeys(t *testing.T) { check(keys[7], curses.AltZ+'J') check(keys[8], curses.AltZ+'g') } + +func TestParseKeysWithComma(t *testing.T) { + check := func(key int, expected int) { + if key != expected { + t.Errorf("%d != %d", key, expected) + } + } + + keys := parseKeyChords(",", "") + check(len(keys), 1) + check(keys[0], curses.AltZ+',') + + keys = parseKeyChords(",,a,b", "") + check(len(keys), 3) + check(keys[0], curses.AltZ+'a') + check(keys[1], curses.AltZ+'b') + check(keys[2], curses.AltZ+',') + + keys = parseKeyChords("a,b,,", "") + check(len(keys), 3) + check(keys[0], curses.AltZ+'a') + check(keys[1], curses.AltZ+'b') + check(keys[2], curses.AltZ+',') + + keys = parseKeyChords("a,,,b", "") + check(len(keys), 3) + check(keys[0], curses.AltZ+'a') + check(keys[1], curses.AltZ+'b') + check(keys[2], curses.AltZ+',') + + keys = parseKeyChords("a,,,b,c", "") + check(len(keys), 4) + check(keys[0], curses.AltZ+'a') + check(keys[1], curses.AltZ+'b') + check(keys[2], curses.AltZ+'c') + check(keys[3], curses.AltZ+',') + + keys = parseKeyChords(",,,", "") + check(len(keys), 1) + check(keys[0], curses.AltZ+',') +} diff --git a/src/pattern.go b/src/pattern.go index 7acdbcf..fbb70c5 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -54,17 +54,21 @@ var ( ) func init() { - // We can uniquely identify the pattern for a given string since - // mode and caseMode do not change while the program is running - _patternCache = make(map[string]*Pattern) _splitRegex = regexp.MustCompile("\\s+") - _cache = NewChunkCache() + clearPatternCache() + clearChunkCache() } func clearPatternCache() { + // We can uniquely identify the pattern for a given string since + // mode and caseMode do not change while the program is running _patternCache = make(map[string]*Pattern) } +func clearChunkCache() { + _cache = NewChunkCache() +} + // BuildPattern builds Pattern object from the given arguments func BuildPattern(mode Mode, caseMode Case, nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern { diff --git a/src/terminal.go b/src/terminal.go index 2d191a9..d027d76 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -28,6 +28,7 @@ type Terminal struct { yanked []rune input []rune multi bool + toggleSort int expect []int pressed int printQuery bool @@ -93,6 +94,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { yanked: []rune{}, input: input, multi: opts.Multi, + toggleSort: opts.ToggleSort, expect: opts.Expect, pressed: 0, printQuery: opts.PrintQuery, @@ -457,6 +459,10 @@ func (t *Terminal) rubout(pattern string) { t.input = append(t.input[:t.cx], after...) } +func keyMatch(key int, event C.Event) bool { + return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ +} + // Loop is called to start Terminal I/O func (t *Terminal) Loop() { <-t.startChan @@ -553,12 +559,19 @@ func (t *Terminal) Loop() { } } for _, key := range t.expect { - if event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ { + if keyMatch(key, event) { t.pressed = key req(reqClose) break } } + if t.toggleSort > 0 { + if keyMatch(t.toggleSort, event) { + t.eventBox.Set(EvtSearchNew, true) + t.mutex.Unlock() + continue + } + } switch event.Type { case C.Invalid: t.mutex.Unlock() @@ -688,7 +701,7 @@ func (t *Terminal) Loop() { t.mutex.Unlock() // Must be unlocked before touching reqBox if changed { - t.eventBox.Set(EvtSearchNew, nil) + t.eventBox.Set(EvtSearchNew, false) } for _, event := range events { t.reqBox.Set(event, nil) diff --git a/test/test_go.rb b/test/test_go.rb index a47e422..ebedbff 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -457,6 +457,19 @@ class TestGoFZF < TestBase tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter assert_equal ['55', '', '55'], readonce.split($/) end + + def test_toggle_sort + tmux.send_keys "seq 1 111 | #{fzf '-m +s --tac --toggle-sort=ctrl-r -q11'}", :Enter + tmux.until { |lines| lines[-3].include? '> 111' } + tmux.send_keys :Tab + tmux.until { |lines| lines[-2].include? '4/111 (1)' } + tmux.send_keys 'C-R' + tmux.until { |lines| lines[-3].include? '> 11' } + tmux.send_keys :Tab + tmux.until { |lines| lines[-2].include? '4/111 (2)' } + tmux.send_keys :Enter + assert_equal ['111', '11'], readonce.split($/) + end end module TestShell From 467a22dd36be174c7fca0f73a545d6c5f66d5c5e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 31 Mar 2015 22:09:04 +0900 Subject: [PATCH 164/408] Period. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6070f9a..b070724 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ fish. - `CTRL-T` - Paste the selected file path(s) into the command line - `CTRL-R` - Paste the selected command from history into the command line - - Sort is disabled by default. Press `CTRL-R` again to toggle sort + - Sort is disabled by default. Press `CTRL-R` again to toggle sort. - `ALT-C` - cd into the selected directory If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You From dd4d465305d613d81f9e9ed90f385a61a1b28398 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 3 Apr 2015 01:21:50 +0900 Subject: [PATCH 165/408] Update Homebrew instruction Close #175 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b070724..006ddc4 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,10 @@ curl -L https://github.com/junegunn/fzf/archive/master.tar.gz | On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. ```sh -brew install fzf +brew reinstall --HEAD fzf -# Install shell extensions - this should be done whenever fzf is updated -$(brew info fzf | grep /install) +# Install shell extensions +/usr/local/Cellar/fzf/HEAD/install ``` #### Install as Vim plugin From 57319f8c58e23257058a2e698b1f8c612db68d8d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 4 Apr 2015 09:18:04 +0900 Subject: [PATCH 166/408] [vim] Fix #177 - :FZF with relative paths --- plugin/fzf.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 94df15f..479fc7d 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -160,6 +160,7 @@ function! s:pushd(dict) if s:present(a:dict, 'dir') let a:dict.prev_dir = getcwd() execute 'chdir '.s:escape(a:dict.dir) + let a:dict.dir = getcwd() return 1 endif return 0 From 68503d32df91ded701cf7ad07f340aade4aa2e1d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 4 Apr 2015 11:55:57 +0900 Subject: [PATCH 167/408] [vim] Code cleanup --- plugin/fzf.vim | 24 +++++++++--------------- test/fzf.vader | 9 ++++++--- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 479fc7d..ac17a41 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -125,11 +125,15 @@ function! fzf#run(...) abort let split = s:tmux_enabled() && s:tmux_splittable(dict) let command = prefix.(split ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result - if split - return s:execute_tmux(dict, command, temps) - else - return s:execute(dict, command, temps) - endif + try + if split + return s:execute_tmux(dict, command, temps) + else + return s:execute(dict, command, temps) + endif + finally + call s:popd(dict) + endtry endfunction function! s:present(dict, ...) @@ -195,14 +199,6 @@ function! s:execute(dict, command, temps) endif endfunction -function! s:env_var(name) - if exists('$'.a:name) - return a:name . "='". substitute(expand('$'.a:name), "'", "'\\\\''", 'g') . "' " - else - return '' - endif -endfunction - function! s:execute_tmux(dict, command, temps) let command = a:command if s:pushd(a:dict) @@ -234,8 +230,6 @@ function! s:callback(dict, temps) silent! call delete(tf) endfor - call s:popd(a:dict) - return lines endfunction diff --git a/test/fzf.vader b/test/fzf.vader index 74dbde1..c27fddc 100644 --- a/test/fzf.vader +++ b/test/fzf.vader @@ -3,11 +3,14 @@ Execute (Setup): Log 'Test directory: ' . g:dir Execute (fzf#run with dir option): + let cwd = getcwd() let result = fzf#run({ 'options': '--filter=vdr', 'dir': g:dir }) AssertEqual ['fzf.vader'], result + AssertEqual getcwd(), cwd let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir })) - AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result + AssertEqual ['fzf.vader', 'test_go.rb'], result + AssertEqual getcwd(), cwd Execute (fzf#run with Funcref command): let g:ret = [] @@ -15,8 +18,8 @@ Execute (fzf#run with Funcref command): call add(g:ret, a:e) endfunction let result = sort(fzf#run({ 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir })) - AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result - AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], sort(g:ret) + AssertEqual ['fzf.vader', 'test_go.rb'], result + AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret) Execute (fzf#run with string source): let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' })) From 622e69ff54f27363abc2ac18a5a69751f2b42f4a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 10 Apr 2015 23:23:47 +0900 Subject: [PATCH 168/408] [vim] Neovim compatibility (#137) Use terminal emulator of Neovim to open fzf --- README.md | 5 +++ plugin/fzf.vim | 116 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 97 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 006ddc4..5a47174 100644 --- a/README.md +++ b/README.md @@ -317,11 +317,16 @@ of the selected items. | `source` | list | Vim list as input to fzf | | `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) | | `sink` | funcref | Reference to function to process each selected item | +| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once | | `options` | string | Options to fzf | | `dir` | string | Working directory | | `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | +| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) | | `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) | +*However on Neovim `fzf#run` is asynchronous and does not return values so you +should use `sink` or `sink+` to process the output from fzf.* + ##### Examples If `sink` option is not given, `fzf#run` will simply return the list. diff --git a/plugin/fzf.vim b/plugin/fzf.vim index ac17a41..9a942a0 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -122,12 +122,14 @@ function! fzf#run(...) abort else let prefix = '' endif - let split = s:tmux_enabled() && s:tmux_splittable(dict) - let command = prefix.(split ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result + let tmux = !has('nvim') && s:tmux_enabled() && s:splittable(dict) + let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result try - if split + if tmux return s:execute_tmux(dict, command, temps) + elseif has('nvim') + return s:execute_term(dict, command, temps) else return s:execute(dict, command, temps) endif @@ -150,19 +152,24 @@ function! s:fzf_tmux(dict) for o in ['up', 'down', 'left', 'right'] if s:present(a:dict, o) let size = '-'.o[0].(a:dict[o] == 1 ? '' : a:dict[o]) + break endif endfor return printf('LINES=%d COLUMNS=%d %s %s %s --', \ &lines, &columns, s:fzf_tmux, size, (has_key(a:dict, 'source') ? '' : '-')) endfunction -function! s:tmux_splittable(dict) +function! s:splittable(dict) return s:present(a:dict, 'up', 'down', 'left', 'right') endfunction function! s:pushd(dict) if s:present(a:dict, 'dir') - let a:dict.prev_dir = getcwd() + let cwd = getcwd() + if get(a:dict, 'prev_dir', '') ==# cwd + return 1 + endif + let a:dict.prev_dir = cwd execute 'chdir '.s:escape(a:dict.dir) let a:dict.dir = getcwd() return 1 @@ -210,6 +217,60 @@ function! s:execute_tmux(dict, command, temps) return s:callback(a:dict, a:temps) endfunction +function! s:calc_size(max, val) + if a:val =~ '%$' + return a:max * str2nr(a:val[:-2]) / 100 + else + return min([a:max, a:val]) + endif +endfunction + +function! s:split(dict) + let directions = { + \ 'up': ['topleft', &lines], + \ 'down': ['botright', &lines], + \ 'left': ['vertical topleft', &columns], + \ 'right': ['vertical botright', &columns] } + try + for [dir, pair] in items(directions) + let val = get(a:dict, dir, '') + if !empty(val) + let [cmd, max] = pair + execute cmd s:calc_size(max, val).'new' + return + endif + endfor + if s:present(a:dict, 'window') + execute a:dict.window + else + tabnew + endif + finally + setlocal winfixwidth winfixheight + endtry +endfunction + +function! s:execute_term(dict, command, temps) + call s:pushd(a:dict) + call s:split(a:dict) + + let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps } + function! fzf.on_exit(id, code) + execute 'bd!' self.buf + call s:pushd(self.dict) + try + call s:callback(self.dict, self.temps) + finally + call s:popd(self.dict) + endtry + endfunction + + call termopen(a:command, fzf) + file [FZF] + startinsert + return [] +endfunction + function! s:callback(dict, temps) if !filereadable(a:temps.result) let lines = [] @@ -224,6 +285,9 @@ function! s:callback(dict, temps) endif endfor endif + if has_key(a:dict, 'sink*') + call a:dict['sink*'](lines) + endif endif for tf in values(a:temps) @@ -233,6 +297,26 @@ function! s:callback(dict, temps) return lines endfunction +function! s:cmd_callback(lines) abort + if empty(a:lines) + return + endif + let key = remove(a:lines, 0) + if key == 'ctrl-t' | let cmd = 'tabedit' + elseif key == 'ctrl-x' | let cmd = 'split' + elseif key == 'ctrl-v' | let cmd = 'vsplit' + else | let cmd = 'e' + endif + call s:pushd(s:opts) + try + for item in a:lines + execute cmd s:escape(item) + endfor + finally + call s:popd(s:opts) + endtry +endfunction + function! s:cmd(bang, ...) abort let args = copy(a:000) if !s:legacy @@ -247,26 +331,10 @@ function! s:cmd(bang, ...) abort endif if s:legacy - call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts)) + call fzf#run(extend({ 'options': join(args), 'sink': 'e' }, opts)) else - let output = fzf#run(extend({ 'options': join(args) }, opts)) - if empty(output) - return - endif - let key = remove(output, 0) - if key == 'ctrl-t' | let cmd = 'tabedit' - elseif key == 'ctrl-x' | let cmd = 'split' - elseif key == 'ctrl-v' | let cmd = 'vsplit' - else | let cmd = 'e' - endif - try - call s:pushd(opts) - for item in output - execute cmd s:escape(item) - endfor - finally - call s:popd(opts) - endtry + let s:opts = opts + call fzf#run(extend({ 'options': join(args), 'sink*': function('cmd_callback') }, opts)) endif endfunction From c1adf0cd3df8b6ce4724678976eac6d87318c38d Mon Sep 17 00:00:00 2001 From: Marco Hinz Date: Fri, 10 Apr 2015 22:18:46 +0200 Subject: [PATCH 169/408] Check if :FZF is already executing Prior to this change, you'd get a longer error message if you did: :FZF :FZF The main problem being that `:file [FZF]` can be used only once. --- plugin/fzf.vim | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 9a942a0..2b56920 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -318,6 +318,12 @@ function! s:cmd_callback(lines) abort endfunction function! s:cmd(bang, ...) abort + if bufexists('[FZF]') + echohl WarningMsg + echomsg 'FZF is already running!' + echohl NONE + return + endif let args = copy(a:000) if !s:legacy let args = insert(args, '--expect=ctrl-t,ctrl-x,ctrl-v', 0) From 2cec5c0f304a4ba766fb7ea0506b707c54fcc32e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 11 Apr 2015 09:21:23 +0900 Subject: [PATCH 170/408] Fix typo in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5a47174..8e02d88 100644 --- a/README.md +++ b/README.md @@ -324,8 +324,8 @@ of the selected items. | `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) | | `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) | -*However on Neovim `fzf#run` is asynchronous and does not return values so you -should use `sink` or `sink+` to process the output from fzf.* +_However on Neovim `fzf#run` is asynchronous and does not return values so you +should use `sink` or `sink*` to process the output from fzf._ ##### Examples From 1481304d3bcf6408d0bd6914bfef207ee38fa308 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 11 Apr 2015 11:19:22 +0900 Subject: [PATCH 171/408] Suppress message from :file Suggested by @noahfrederick --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 2b56920..3a30a37 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -266,7 +266,7 @@ function! s:execute_term(dict, command, temps) endfunction call termopen(a:command, fzf) - file [FZF] + silent file [FZF] startinsert return [] endfunction From f22b83db6c8ab6b83373381fa97b3b4e1c89b640 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 11 Apr 2015 11:28:30 +0900 Subject: [PATCH 172/408] Update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8e02d88..fca842b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ fzf project consists of the followings: - Shell extensions - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) - Fuzzy auto-completion (bash only) +- Vim/Neovim plugin You can [download fzf executable][bin] alone, but it's recommended that you install the extra stuff using the attached install script. From 8406cedf2d8755729636bf788037b916c52483f4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 11 Apr 2015 23:42:01 +0900 Subject: [PATCH 173/408] [vim] Improved compatibility with sidebar plugins (e.g. NERDtree) --- plugin/fzf.vim | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 3a30a37..fc42bb0 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -227,16 +227,19 @@ endfunction function! s:split(dict) let directions = { - \ 'up': ['topleft', &lines], - \ 'down': ['botright', &lines], - \ 'left': ['vertical topleft', &columns], - \ 'right': ['vertical botright', &columns] } + \ 'up': ['topleft', 'resize', &lines], + \ 'down': ['botright', 'resize', &lines], + \ 'left': ['vertical topleft', 'vertical resize', &columns], + \ 'right': ['vertical botright', 'vertical resize', &columns] } + let s:ptab = tabpagenr() try - for [dir, pair] in items(directions) + for [dir, triple] in items(directions) let val = get(a:dict, dir, '') if !empty(val) - let [cmd, max] = pair - execute cmd s:calc_size(max, val).'new' + let [cmd, resz, max] = triple + let sz = s:calc_size(max, val) + execute cmd sz.'new' + execute resz sz return endif endfor @@ -256,7 +259,11 @@ function! s:execute_term(dict, command, temps) let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps } function! fzf.on_exit(id, code) + let tab = tabpagenr() execute 'bd!' self.buf + if s:ptab == tab + wincmd p + endif call s:pushd(self.dict) try call s:callback(self.dict, self.temps) From 8385a55bda0e1eb8273fc0598010a9f0fb6d1710 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 11 Apr 2015 23:47:46 +0900 Subject: [PATCH 174/408] [vim] s:pushd after s:split It is possible that the user has an autocmd that changes the current directory. --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index fc42bb0..2066d70 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -254,8 +254,8 @@ function! s:split(dict) endfunction function! s:execute_term(dict, command, temps) - call s:pushd(a:dict) call s:split(a:dict) + call s:pushd(a:dict) let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps } function! fzf.on_exit(id, code) From 797f42ecc614f732080de70b1006616f435de4f0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 12 Apr 2015 00:44:41 +0900 Subject: [PATCH 175/408] Update README --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index fca842b..6f39e88 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -fzf - a command-line fuzzy finder [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) +fzf - a command-line fuzzy finder [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) Flattr this === fzf is a general-purpose command-line fuzzy finder. @@ -16,7 +16,7 @@ Pros - The most comprehensive feature set - Try `fzf --help` and be surprised - Batteries included - - Vim plugin, key bindings and fuzzy auto-completion + - Vim/Neovim plugin, key bindings and fuzzy auto-completion Installation ------------ @@ -89,7 +89,7 @@ while. Please follow the instruction below depending on the installation method. - git: `cd ~/.fzf && git pull && ./install` -- brew: `brew update && brew upgrade fzf && $(brew info fzf | grep /install)` +- brew: `brew reinstall --HEAD fzf` - vim-plug: `:PlugUpdate fzf` Usage @@ -153,7 +153,8 @@ fish. - `CTRL-T` - Paste the selected file path(s) into the command line - `CTRL-R` - Paste the selected command from history into the command line - - Sort is disabled by default. Press `CTRL-R` again to toggle sort. + - Sort is disabled by default to respect chronological ordering + - Press `CTRL-R` again to toggle sort - `ALT-C` - cd into the selected directory If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You @@ -301,9 +302,8 @@ let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s' If you're running MacVim on OSX, I recommend you to use iTerm2 as the launcher. Refer to the [this wiki -page](https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2) to see - -how to set up. +page](https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2) to see how +to set up. #### `fzf#run([options])` @@ -364,22 +364,22 @@ handy mapping that selects an open buffer. ```vim " List of buffers -function! BufList() +function! s:buflist() redir => ls silent ls redir END return split(ls, '\n') endfunction -function! BufOpen(e) - execute 'buffer '. matchstr(a:e, '^[ 0-9]*') +function! s:bufopen(e) + execute 'buffer' matchstr(a:e, '^[ 0-9]*') endfunction nnoremap :call fzf#run({ -\ 'source': reverse(BufList()), -\ 'sink': function('BufOpen'), +\ 'source': reverse(buflist()), +\ 'sink': function('bufopen'), \ 'options': '+m', -\ 'down': '40%' +\ 'down': len(buflist()) + 2 \ }) ``` From 348731fc3b1e3a8f653522c39e61f78ffc7967fb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 12 Apr 2015 01:57:05 +0900 Subject: [PATCH 176/408] Make fzf-tmux work when fzf is not in $PATH but in the same directory See: #181 --- bin/fzf-tmux | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index f1fcb09..55ea5f7 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -106,7 +106,9 @@ fail() { >&2 echo "$1" exit 1 } -fzf=$(which fzf 2> /dev/null) || fail "fzf executable not found" +fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" +[ -x "$fzf" ] || fail "fzf executable not found" + envs="" [ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" From 82bea6758a99a1c6b9ac13f92e0959a851604f5a Mon Sep 17 00:00:00 2001 From: Bryan Head Date: Sat, 11 Apr 2015 12:27:50 -0500 Subject: [PATCH 177/408] Move active check to fzf#run. --- plugin/fzf.vim | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 2066d70..135a3a2 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -98,6 +98,12 @@ function! s:upgrade(dict) endfunction function! fzf#run(...) abort + if has('nvim') && bufexists('[FZF]') + echohl WarningMsg + echomsg 'FZF is already running!' + echohl NONE + return [] + endif let dict = exists('a:1') ? s:upgrade(a:1) : {} let temps = { 'result': tempname() } let optstr = get(dict, 'options', '') @@ -325,12 +331,6 @@ function! s:cmd_callback(lines) abort endfunction function! s:cmd(bang, ...) abort - if bufexists('[FZF]') - echohl WarningMsg - echomsg 'FZF is already running!' - echohl NONE - return - endif let args = copy(a:000) if !s:legacy let args = insert(args, '--expect=ctrl-t,ctrl-x,ctrl-v', 0) From a4f3d09704364b6c1c496fa465cc4dc594f44159 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Apr 2015 00:59:45 +0900 Subject: [PATCH 178/408] Fuzzy completion using fzf-tmux --- README.md | 3 +++ shell/completion.bash | 16 ++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6f39e88..7b1bb4c 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,9 @@ export FZF_COMPLETION_TRIGGER='~~' # Options to fzf command export FZF_COMPLETION_OPTS='+c -x' + +# Not to use tmux pane +export FZF_COMPLETION_TMUX=0 ``` Usage as Vim plugin diff --git a/shell/completion.bash b/shell/completion.bash index e609d2b..1a4426e 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -6,6 +6,7 @@ # /_/ /___/_/-completion.bash # # - $FZF_COMPLETION_TRIGGER (default: '**') +# - $FZF_COMPLETION_TMUX (default: 1) # - $FZF_COMPLETION_OPTS (default: empty) _fzf_orig_completion_filter() { @@ -65,7 +66,8 @@ _fzf_handle_dynamic_completion() { } _fzf_path_completion() { - local cur base dir leftover matches trigger cmd + local cur base dir leftover matches trigger cmd fzf + [ ${FZF_COMPLETION_TMUX:-1} -eq 1 ] && fzf="fzf-tmux" || fzf="fzf" cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') COMPREPLY=() trigger=${FZF_COMPLETION_TRIGGER:-**} @@ -81,7 +83,7 @@ _fzf_path_completion() { leftover=${leftover/#\/} [ "$dir" = './' ] && dir='' tput sc - matches=$(find -L "$dir"* $1 2> /dev/null | fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do + matches=$(find -L "$dir"* $1 2> /dev/null | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do printf "%q$3 " "$item" done) matches=${matches% } @@ -105,7 +107,8 @@ _fzf_path_completion() { } _fzf_list_completion() { - local cur selected trigger cmd src + local cur selected trigger cmd src fzf + [ ${FZF_COMPLETION_TMUX:-1} -eq 1 ] && fzf="fzf-tmux" || fzf="fzf" read -r src cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') trigger=${FZF_COMPLETION_TRIGGER:-**} @@ -114,7 +117,7 @@ _fzf_list_completion() { cur=${cur:0:${#cur}-${#trigger}} tput sc - selected=$(eval "$src | fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ') + selected=$(eval "$src | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ') selected=${selected% } tput rc @@ -149,9 +152,10 @@ _fzf_dir_completion() { _fzf_kill_completion() { [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 - local selected + local selected fzf + [ ${FZF_COMPLETION_TMUX:-1} -eq 1 ] && fzf="fzf-tmux" || fzf="fzf" tput sc - selected=$(ps -ef | sed 1d | fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') + selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') tput rc if [ -n "$selected" ]; then From 91fc6c984b8b8eaf793afc778efaa624c7c35527 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Apr 2015 02:00:50 +0900 Subject: [PATCH 179/408] Fix fuzzy completion test --- test/test_go.rb | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index ebedbff..94093ff 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -546,25 +546,29 @@ class TestBash < TestBase def test_file_completion tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter tmux.prepare - tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab - tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 + tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } tmux.send_keys :BTab, :BTab, :Enter - tmux.until { |lines| + tmux.until do |lines| + tmux.send_keys 'C-L' lines[-1].include?('/tmp/fzf-test/10') && lines[-1].include?('/tmp/fzf-test/100') - } + end end def test_dir_completion tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter tmux.prepare - tmux.send_keys 'cd /tmp/fzf-test/**', :Tab - tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0 + tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } tmux.send_keys :BTab, :BTab # BTab does not work here tmux.send_keys 55 - tmux.until { |lines| lines[-2].start_with? ' 1/' } + tmux.until(pane: 1) { |lines| lines[-2].start_with? ' 1/' } tmux.send_keys :Enter - tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' } + tmux.until do |lines| + tmux.send_keys 'C-L' + lines[-1] == 'cd /tmp/fzf-test/d55/' + end tmux.send_keys :xx tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } @@ -584,12 +588,15 @@ class TestBash < TestBase lines = tmux.until { |lines| lines[-1].start_with? '[1]' } pid = lines[-1].split.last tmux.prepare - tmux.send_keys 'kill ', :Tab - tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys 'kill ', :Tab, pane: 0 + tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } tmux.send_keys 'sleep12345' - tmux.until { |lines| lines[-3].include? 'sleep 12345' } + tmux.until(pane: 1) { |lines| lines[-3].include? 'sleep 12345' } tmux.send_keys :Enter - tmux.until { |lines| lines[-1] == "kill #{pid}" } + tmux.until do |lines| + tmux.send_keys 'C-L' + lines[-1] == "kill #{pid}" + end end end From 3ec83babacac24341f3db92c2a9b8bae4782fa07 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Apr 2015 02:12:45 +0900 Subject: [PATCH 180/408] FZF_TMUX and FZF_TMUX_HEIGHT for fuzzy completion --- README.md | 3 --- shell/completion.bash | 9 +++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7b1bb4c..6f39e88 100644 --- a/README.md +++ b/README.md @@ -256,9 +256,6 @@ export FZF_COMPLETION_TRIGGER='~~' # Options to fzf command export FZF_COMPLETION_OPTS='+c -x' - -# Not to use tmux pane -export FZF_COMPLETION_TMUX=0 ``` Usage as Vim plugin diff --git a/shell/completion.bash b/shell/completion.bash index 1a4426e..04287a6 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -5,8 +5,9 @@ # / __/ / /_/ __/ # /_/ /___/_/-completion.bash # +# - $FZF_TMUX (default: 1) +# - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_COMPLETION_TRIGGER (default: '**') -# - $FZF_COMPLETION_TMUX (default: 1) # - $FZF_COMPLETION_OPTS (default: empty) _fzf_orig_completion_filter() { @@ -67,7 +68,7 @@ _fzf_handle_dynamic_completion() { _fzf_path_completion() { local cur base dir leftover matches trigger cmd fzf - [ ${FZF_COMPLETION_TMUX:-1} -eq 1 ] && fzf="fzf-tmux" || fzf="fzf" + [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') COMPREPLY=() trigger=${FZF_COMPLETION_TRIGGER:-**} @@ -108,7 +109,7 @@ _fzf_path_completion() { _fzf_list_completion() { local cur selected trigger cmd src fzf - [ ${FZF_COMPLETION_TMUX:-1} -eq 1 ] && fzf="fzf-tmux" || fzf="fzf" + [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" read -r src cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') trigger=${FZF_COMPLETION_TRIGGER:-**} @@ -153,7 +154,7 @@ _fzf_kill_completion() { [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 local selected fzf - [ ${FZF_COMPLETION_TMUX:-1} -eq 1 ] && fzf="fzf-tmux" || fzf="fzf" + [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" tput sc selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') tput rc From d4daece76b4ecf6df70763ecb736dd5bd253f5df Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 13 Apr 2015 20:05:45 -0400 Subject: [PATCH 181/408] install: wait for LF in non-interactive shell "read -n 1 ..." ignores all but the first character of a line-delimited stream (e.g. "yes n | ./install"). --- install | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/install b/install index a8bb074..3b8dbe6 100755 --- a/install +++ b/install @@ -5,8 +5,15 @@ version=0.9.7 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) +# If stdin is a tty, we are "interactive". +[ -t 0 ] && interactive=yes + ask() { - read -p "$1 ([y]/n) " -n 1 -r + # non-interactive shell: wait for a linefeed + # interactive shell: continue after a single keypress + [ -n "$interactive" ] && read_n='-n 1' || read_n= + + read -p "$1 ([y]/n) " $read_n -r echo [[ ! $REPLY =~ ^[Nn]$ ]] } From 51a19a280416577a2175a7b778a7ab57a0601dd6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Apr 2015 10:35:51 +0900 Subject: [PATCH 182/408] [vim] Remove unnecessary pushd/popd in :FZF It is already handled by its caller. --- plugin/fzf.vim | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 135a3a2..00f0e19 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -320,14 +320,9 @@ function! s:cmd_callback(lines) abort elseif key == 'ctrl-v' | let cmd = 'vsplit' else | let cmd = 'e' endif - call s:pushd(s:opts) - try - for item in a:lines - execute cmd s:escape(item) - endfor - finally - call s:popd(s:opts) - endtry + for item in a:lines + execute cmd s:escape(item) + endfor endfunction function! s:cmd(bang, ...) abort @@ -346,7 +341,6 @@ function! s:cmd(bang, ...) abort if s:legacy call fzf#run(extend({ 'options': join(args), 'sink': 'e' }, opts)) else - let s:opts = opts call fzf#run(extend({ 'options': join(args), 'sink*': function('cmd_callback') }, opts)) endif endfunction From 319d6ced80712eedc818d6d9f9982be4860b2c01 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Apr 2015 10:46:20 +0900 Subject: [PATCH 183/408] [vim] Simplify :FZF Ruby version can also accept `--expect` option although it's ignored. --- plugin/fzf.vim | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 00f0e19..f08e017 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -26,7 +26,6 @@ let s:launcher = 'xterm -e bash -ic %s' let s:fzf_go = expand(':h:h').'/bin/fzf' let s:fzf_rb = expand(':h:h').'/fzf' let s:fzf_tmux = expand(':h:h').'/bin/fzf-tmux' -let s:legacy = 0 let s:cpo_save = &cpo set cpo&vim @@ -41,7 +40,6 @@ function! s:fzf_exec() let s:exec = path[0] elseif executable(s:fzf_rb) let s:exec = s:fzf_rb - let s:legacy = 1 else call system('type fzf') if v:shell_error @@ -326,10 +324,7 @@ function! s:cmd_callback(lines) abort endfunction function! s:cmd(bang, ...) abort - let args = copy(a:000) - if !s:legacy - let args = insert(args, '--expect=ctrl-t,ctrl-x,ctrl-v', 0) - endif + let args = extend(['--expect=ctrl-t,ctrl-x,ctrl-v'], a:000) let opts = {} if len(args) > 0 && isdirectory(expand(args[-1])) let opts.dir = remove(args, -1) @@ -337,15 +332,10 @@ function! s:cmd(bang, ...) abort if !a:bang let opts.down = get(g:, 'fzf_tmux_height', s:default_tmux_height) endif - - if s:legacy - call fzf#run(extend({ 'options': join(args), 'sink': 'e' }, opts)) - else - call fzf#run(extend({ 'options': join(args), 'sink*': function('cmd_callback') }, opts)) - endif + call fzf#run(extend({'options': join(args), 'sink*': function('cmd_callback')}, opts)) endfunction -command! -nargs=* -complete=dir -bang FZF call s:cmd('' == '!', ) +command! -nargs=* -complete=dir -bang FZF call s:cmd(0, ) let &cpo = s:cpo_save unlet s:cpo_save From 5c25984ea01677eb759501a5cff3fa63d2d460d8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Apr 2015 21:45:37 +0900 Subject: [PATCH 184/408] Fix Unicode case handling (#186) --- CHANGELOG.md | 7 +++++++ install | 2 +- src/algo/algo.go | 37 +++++++++++++++++++++++++------------ src/constants.go | 2 +- src/pattern.go | 12 +++++++++--- test/test_go.rb | 7 +++++++ 6 files changed, 50 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b1cf3..edaf674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +0.9.8 +----- + +### Bug fixes + +- Fixed Unicode case handling (#186) + 0.9.7 ----- diff --git a/install b/install index 3b8dbe6..c47b6ec 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.7 +version=0.9.8 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/src/algo/algo.go b/src/algo/algo.go index 60c436e..36c8d87 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -1,6 +1,9 @@ package algo -import "strings" +import ( + "strings" + "unicode" +) /* * String matching algorithms here do not use strings.ToLower to avoid @@ -34,8 +37,17 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { for index, char := range runes { // This is considerably faster than blindly applying strings.ToLower to the // whole string - if !caseSensitive && char >= 65 && char <= 90 { - char += 32 + if !caseSensitive { + // Partially inlining `unicode.ToLower`. Ugly, but makes a noticeable + // difference in CPU cost. (Measured on Go 1.4.1. Also note that the Go + // compiler as of now does not inline non-leaf functions.) + if char >= 'A' && char <= 'Z' { + char += 32 + runes[index] = char + } else if char > unicode.MaxASCII { + char = unicode.To(unicode.LowerCase, char) + runes[index] = char + } } if char == pattern[pidx] { if sidx < 0 { @@ -52,9 +64,6 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { pidx-- for index := eidx - 1; index >= sidx; index-- { char := runes[index] - if !caseSensitive && char >= 65 && char <= 90 { - char += 32 - } if char == pattern[pidx] { if pidx--; pidx < 0 { sidx = index @@ -110,8 +119,12 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in pidx := 0 for index := 0; index < numRunes; index++ { char := runes[index] - if !caseSensitive && char >= 65 && char <= 90 { - char += 32 + if !caseSensitive { + if char >= 'A' && char <= 'Z' { + char += 32 + } else if char > unicode.MaxASCII { + char = unicode.To(unicode.LowerCase, char) + } } if pattern[pidx] == char { pidx++ @@ -135,8 +148,8 @@ func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { for index, r := range pattern { char := runes[index] - if !caseSensitive && char >= 65 && char <= 90 { - char += 32 + if !caseSensitive { + char = unicode.ToLower(char) } if char != r { return -1, -1 @@ -156,8 +169,8 @@ func SuffixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { for index, r := range pattern { char := runes[index+diff] - if !caseSensitive && char >= 65 && char <= 90 { - char += 32 + if !caseSensitive { + char = unicode.ToLower(char) } if char != r { return -1, -1 diff --git a/src/constants.go b/src/constants.go index 5cd6d80..36804d0 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.7" +const Version = "0.9.8" // fzf events const ( diff --git a/src/pattern.go b/src/pattern.go index fbb70c5..e6bda5f 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -4,12 +4,11 @@ import ( "regexp" "sort" "strings" + "unicode" "github.com/junegunn/fzf/src/algo" ) -const uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - // fuzzy // 'exact // ^exact-prefix @@ -91,7 +90,14 @@ func BuildPattern(mode Mode, caseMode Case, switch caseMode { case CaseSmart: - if !strings.ContainsAny(asString, uppercaseLetters) { + hasUppercase := false + for _, r := range runes { + if unicode.IsUpper(r) { + hasUppercase = true + break + } + } + if !hasUppercase { runes, caseSensitive = []rune(strings.ToLower(asString)), false } case CaseIgnore: diff --git a/test/test_go.rb b/test/test_go.rb index 94093ff..a4b1970 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -470,6 +470,13 @@ class TestGoFZF < TestBase tmux.send_keys :Enter assert_equal ['111', '11'], readonce.split($/) end + + def test_unicode_case + assert_equal %w[СТРОКА2 Строка4], + `printf "строКА1\\nСТРОКА2\\nстрока3\\nСтрока4" | fzf -fС`.split($/) + assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], + `printf "строКА1\\nСТРОКА2\\nстрока3\\nСтрока4" | fzf -fс`.split($/) + end end module TestShell From 3a9d1df0264226f953698c03914575d5f6b1d974 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Apr 2015 21:59:44 +0900 Subject: [PATCH 185/408] Fix unicode test case --- test/test_go.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index a4b1970..d98ab8b 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -473,9 +473,9 @@ class TestGoFZF < TestBase def test_unicode_case assert_equal %w[СТРОКА2 Строка4], - `printf "строКА1\\nСТРОКА2\\nстрока3\\nСтрока4" | fzf -fС`.split($/) + `ruby -e "puts %w[строКА1 СТРОКА2 строка3 Строка4]" | fzf -fС`.split($/) assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], - `printf "строКА1\\nСТРОКА2\\nстрока3\\nСтрока4" | fzf -fс`.split($/) + `ruby -e "puts %w[строКА1 СТРОКА2 строка3 Строка4]" | fzf -fс`.split($/) end end From 2d68cb8639a78ec6b7e88dd71e2ce1a638e5f8ea Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Apr 2015 23:19:33 +0900 Subject: [PATCH 186/408] Fix #185 - Terminate on RuneError --- CHANGELOG.md | 1 + src/curses/curses.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index edaf674..0fd89e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG ### Bug fixes - Fixed Unicode case handling (#186) +- Fixed to terminate on RuneError (#185) 0.9.7 ----- diff --git a/src/curses/curses.go b/src/curses/curses.go index d6aafd7..f3ad143 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -420,6 +420,9 @@ func GetChar() Event { return Event{int(_buf[0]), 0, nil} } r, rsz := utf8.DecodeRune(_buf) + if r == utf8.RuneError { + return Event{ESC, 0, nil} + } sz = rsz return Event{Rune, r, nil} } From b882de87ab4bbd303dbcd7e5d928862b509a09d1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Apr 2015 22:23:11 +0900 Subject: [PATCH 187/408] Fix Travis CI build --- .travis.yml | 10 +++------- test/test_go.rb | 28 ++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index cce98ff..cbd5c11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,17 +12,13 @@ install: - sudo apt-get install -y zsh fish script: | - export GOROOT=~/go1.4 export GOPATH=~/go - export FZF_BASE=~/go/src/github.com/junegunn/fzf + export FZF_BASE=$GOPATH/src/github.com/junegunn/fzf - mkdir -p ~/go/src/github.com/junegunn + mkdir -p $GOPATH/src/github.com/junegunn ln -s $(pwd) $FZF_BASE - curl https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | tar -xz - mv go $GOROOT - cd $FZF_BASE/src && make test fzf/fzf-linux_amd64 install && cd $FZF_BASE/bin && ln -sf fzf-linux_amd64 fzf-$(./fzf --version)-linux_amd64 && - cd $FZF_BASE && yes | ./install && + cd $FZF_BASE && yes | ./install && rm -f fzf && tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ] diff --git a/test/test_go.rb b/test/test_go.rb index d98ab8b..a9284b6 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -4,7 +4,9 @@ require 'minitest/autorun' require 'fileutils' -Dir.chdir File.expand_path('../../', __FILE__) +base = File.expand_path('../../', __FILE__) +Dir.chdir base +FZF = "#{base}/bin/fzf" class NilClass def include? str @@ -26,7 +28,8 @@ module Temp waited = 0 while waited < 5 begin - data = `cat #{name}` + system 'sync' + data = File.read(name) return data unless data.empty? rescue sleep 0.1 @@ -195,7 +198,7 @@ class TestBase < Minitest::Test nil end }.compact - "fzf #{opts.join ' '}" + "#{FZF} #{opts.join ' '}" end end @@ -243,7 +246,7 @@ class TestGoFZF < TestBase end def test_key_bindings - tmux.send_keys "fzf -q 'foo bar foo-bar'", :Enter + tmux.send_keys "#{FZF} -q 'foo bar foo-bar'", :Enter tmux.until { |lines| lines.last =~ /^>/ } # CTRL-A @@ -472,10 +475,19 @@ class TestGoFZF < TestBase end def test_unicode_case - assert_equal %w[СТРОКА2 Строка4], - `ruby -e "puts %w[строКА1 СТРОКА2 строка3 Строка4]" | fzf -fС`.split($/) - assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], - `ruby -e "puts %w[строКА1 СТРОКА2 строка3 Строка4]" | fzf -fс`.split($/) + tempname = TEMPNAME + Time.now.to_f.to_s + File.open(tempname, 'w') do |f| + f << %w[строКА1 СТРОКА2 строка3 Строка4].join($/) + f.sync + end + since = Time.now + while `cat #{tempname}`.split($/).length != 4 && (Time.now - since) < 10 + sleep 0.1 + end + assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/) + assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/) + rescue + File.unlink tempname end end From 2add45fe2f0b0860eba007948c1cfe734c56192c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 15 Apr 2015 22:32:45 +0900 Subject: [PATCH 188/408] [vim] Rename g:fzf_tmux_height to g:fzf_height Because tmux panes are not used on Neovim. --- README.md | 6 +++--- plugin/fzf.vim | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6f39e88..c0dd8cd 100644 --- a/README.md +++ b/README.md @@ -285,9 +285,9 @@ in new tabs, in horizontal splits, or in vertical splits respectively. Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS` also apply here. -If you're on a tmux session, `:FZF` will launch fzf in a new split-window whose -height can be adjusted with `g:fzf_tmux_height` (default: '40%'). However, the -bang version (`:FZF!`) will always start in fullscreen. +If you're on a tmux session or using Neovim, `:FZF` will launch fzf in a +split-window whose height can be adjusted with `g:fzf_height` (default: +'40%'). However, the bang version (`:FZF!`) will always start in fullscreen. In GVim, you need an external terminal emulator to start fzf with. `xterm` command is used by default, but you can customize it with `g:fzf_launcher`. diff --git a/plugin/fzf.vim b/plugin/fzf.vim index f08e017..063bc2e 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -21,7 +21,7 @@ " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -let s:default_tmux_height = '40%' +let s:default_height = '40%' let s:launcher = 'xterm -e bash -ic %s' let s:fzf_go = expand(':h:h').'/bin/fzf' let s:fzf_rb = expand(':h:h').'/fzf' @@ -330,7 +330,7 @@ function! s:cmd(bang, ...) abort let opts.dir = remove(args, -1) endif if !a:bang - let opts.down = get(g:, 'fzf_tmux_height', s:default_tmux_height) + let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height)) endif call fzf#run(extend({'options': join(args), 'sink*': function('cmd_callback')}, opts)) endfunction From 853012ceef7c8e8e28e6cbb6c790f76956a62b03 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 15 Apr 2015 22:49:45 +0900 Subject: [PATCH 189/408] [vim] Add g:fzf_action for customizing key bindings Close #189 --- README.md | 27 +++++---------------------- plugin/fzf.vim | 15 +++++++++------ 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index c0dd8cd..3337093 100644 --- a/README.md +++ b/README.md @@ -276,34 +276,17 @@ If you have set up fzf for Vim, `:FZF` command will be added. " With options :FZF --no-sort -m /tmp + +" Bang version starts in fullscreen instead of using tmux pane or Neovim split +:FZF! ``` Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key, `CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window, in new tabs, in horizontal splits, or in vertical splits respectively. -Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS` -also apply here. - -If you're on a tmux session or using Neovim, `:FZF` will launch fzf in a -split-window whose height can be adjusted with `g:fzf_height` (default: -'40%'). However, the bang version (`:FZF!`) will always start in fullscreen. - -In GVim, you need an external terminal emulator to start fzf with. `xterm` -command is used by default, but you can customize it with `g:fzf_launcher`. - -```vim -" This is the default. %s is replaced with fzf command -let g:fzf_launcher = 'xterm -e bash -ic %s' - -" Use urxvt instead -let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s' -``` - -If you're running MacVim on OSX, I recommend you to use iTerm2 as the launcher. -Refer to the [this wiki -page](https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2) to see how -to set up. +Note that the environment variables `FZF_DEFAULT_COMMAND` and +`FZF_DEFAULT_OPTS` also apply here. #### `fzf#run([options])` diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 063bc2e..99aa300 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -308,23 +308,26 @@ function! s:callback(dict, temps) return lines endfunction +let s:default_action = { + \ 'ctrl-m': 'e', + \ 'ctrl-t': 'tabedit', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + function! s:cmd_callback(lines) abort if empty(a:lines) return endif let key = remove(a:lines, 0) - if key == 'ctrl-t' | let cmd = 'tabedit' - elseif key == 'ctrl-x' | let cmd = 'split' - elseif key == 'ctrl-v' | let cmd = 'vsplit' - else | let cmd = 'e' - endif + let cmd = get(s:action, key, 'e') for item in a:lines execute cmd s:escape(item) endfor endfunction function! s:cmd(bang, ...) abort - let args = extend(['--expect=ctrl-t,ctrl-x,ctrl-v'], a:000) + let s:action = get(g:, 'fzf_action', s:default_action) + let args = extend(['--expect='.join(keys(s:action), ',')], a:000) let opts = {} if len(args) > 0 && isdirectory(expand(args[-1])) let opts.dir = remove(args, -1) From 120cc0aaddf02c77dcdfadcc2ef8deebcecdf439 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 15 Apr 2015 22:52:15 +0900 Subject: [PATCH 190/408] [vim] README: Pointer to the wiki page --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3337093..9645a8a 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,10 @@ Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key, in new tabs, in horizontal splits, or in vertical splits respectively. Note that the environment variables `FZF_DEFAULT_COMMAND` and -`FZF_DEFAULT_OPTS` also apply here. +`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][vim-examples] for +customization. + +[vim-examples]: https://github.com/junegunn/fzf/wiki/Examples-(vim) #### `fzf#run([options])` From 3e1e75fe089e0bd86c30b83b3b03aaf580788e6c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 10:52:04 +0900 Subject: [PATCH 191/408] Remove unused variable --- src/curses/curses.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index f3ad143..3340a5c 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -116,7 +116,6 @@ var ( _color func(int, bool) C.int _colorMap map[int]int _prevDownTime time.Time - _prevDownY int _clickY []int DarkBG C.short ) From 48ab87294bb2df5ff32ff35a16231991a2e0887b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 12:56:01 +0900 Subject: [PATCH 192/408] Add --no-hscroll option to disable horizontal scroll Close #193 --- man/man1/fzf.1 | 3 +++ src/options.go | 7 +++++++ src/terminal.go | 52 +++++++++++++++++++++++++++++------------------- src/util/util.go | 8 ++++++++ 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index d317adb..0afa4cd 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -89,6 +89,9 @@ Use black background .B "--reverse" Reverse orientation .TP +.B "--no-hscroll" +Disable horizontal scroll +.TP .BI "--prompt=" "STR" Input prompt (default: '> ') .SS Scripting diff --git a/src/options.go b/src/options.go index fcf3097..c186542 100644 --- a/src/options.go +++ b/src/options.go @@ -38,6 +38,7 @@ const usage = `usage: fzf [options] +2, --no-256 Disable 256-color --black Use black background --reverse Reverse orientation + --no-hscroll Disable horizontal scroll --prompt=STR Input prompt (default: '> ') Scripting @@ -93,6 +94,7 @@ type Options struct { Color256 bool Black bool Reverse bool + Hscroll bool Prompt string Query string Select1 bool @@ -121,6 +123,7 @@ func defaultOptions() *Options { Color256: strings.Contains(os.Getenv("TERM"), "256"), Black: false, Reverse: false, + Hscroll: true, Prompt: "> ", Query: "", Select1: false, @@ -304,6 +307,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Reverse = true case "--no-reverse": opts.Reverse = false + case "--hscroll": + opts.Hscroll = true + case "--no-hscroll": + opts.Hscroll = false case "-1", "--select-1": opts.Select1 = true case "+1", "--no-select-1": diff --git a/src/terminal.go b/src/terminal.go index d027d76..b16daec 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -22,6 +22,7 @@ import ( type Terminal struct { prompt string reverse bool + hscroll bool cx int cy int offset int @@ -88,6 +89,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { return &Terminal{ prompt: opts.Prompt, reverse: opts.Reverse, + hscroll: opts.Hscroll, cx: len(input), cy: 0, offset: 0, @@ -318,7 +320,7 @@ func trimLeft(runes []rune, width int) ([]rune, int32) { return runes, trimmed } -func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) { +func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) { var maxe int32 for _, offset := range item.offsets { if offset[1] > maxe { @@ -332,30 +334,40 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, cur maxWidth := C.MaxX() - 3 fullWidth := displayWidth(text) if fullWidth > maxWidth { - // Stri.. - matchEndWidth := displayWidth(text[:maxe]) - if matchEndWidth <= maxWidth-2 { + if t.hscroll { + // Stri.. + matchEndWidth := displayWidth(text[:maxe]) + if matchEndWidth <= maxWidth-2 { + text, _ = trimRight(text, maxWidth-2) + text = append(text, []rune("..")...) + } else { + // Stri.. + if matchEndWidth < fullWidth-2 { + text = append(text[:maxe], []rune("..")...) + } + // ..ri.. + var diff int32 + text, diff = trimLeft(text, maxWidth-2) + + // Transform offsets + for idx, offset := range offsets { + b, e := offset.offset[0], offset.offset[1] + b += 2 - diff + e += 2 - diff + b = util.Max32(b, 2) + offsets[idx].offset[0] = b + offsets[idx].offset[1] = util.Max32(b, e) + } + text = append([]rune(".."), text...) + } + } else { text, _ = trimRight(text, maxWidth-2) text = append(text, []rune("..")...) - } else { - // Stri.. - if matchEndWidth < fullWidth-2 { - text = append(text[:maxe], []rune("..")...) - } - // ..ri.. - var diff int32 - text, diff = trimLeft(text, maxWidth-2) - // Transform offsets for idx, offset := range offsets { - b, e := offset.offset[0], offset.offset[1] - b += 2 - diff - e += 2 - diff - b = util.Max32(b, 2) - offsets[idx].offset[0] = b - offsets[idx].offset[1] = util.Max32(b, e) + offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2)) + offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth)) } - text = append([]rune(".."), text...) } } diff --git a/src/util/util.go b/src/util/util.go index 2d680b1..931b14a 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -19,6 +19,14 @@ func Max(first int, items ...int) int { return max } +// Max32 returns the smallest 32-bit integer +func Min32(first int32, second int32) int32 { + if first <= second { + return first + } + return second +} + // Max32 returns the largest 32-bit integer func Max32(first int32, second int32) int32 { if first > second { From b8904a8c3e8f6d8c00c8d69b153c0d1897b1ade2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 14:19:28 +0900 Subject: [PATCH 193/408] Add --tiebreak option for customizing sort criteria Close #191 --- fzf | 5 ++-- man/man1/fzf.1 | 14 +++++++++++ src/core.go | 1 + src/item.go | 36 +++++++++++++++++++++++---- src/item_test.go | 2 +- src/options.go | 36 +++++++++++++++++++++++++-- test/test_go.rb | 63 ++++++++++++++++++++++++++++++++++++++++++------ 7 files changed, 139 insertions(+), 18 deletions(-) diff --git a/fzf b/fzf index 69bf14f..1bc5673 100755 --- a/fzf +++ b/fzf @@ -206,9 +206,10 @@ class FZF @expect = true when /^--expect=(.*)$/ @expect = true - when '--toggle-sort' + when '--toggle-sort', '--tiebreak' argv.shift - when '--tac', '--sync', '--toggle-sort', /^--toggle-sort=(.*)$/ + when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll', + /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/ # XXX else usage 1, "illegal option: #{o}" diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 0afa4cd..ada0340 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -66,6 +66,20 @@ Reverse the order of the input .RS e.g. \fBhistory | fzf --tac --no-sort\fR .RE +.TP +.BI "--tiebreak=" "STR" +Sort criterion to use when the scores are tied +.br +.R "" +.br +.BR length " Prefers item with shorter length" +.br +.BR begin " Prefers item with matched substring closer to the beginning" +.br +.BR end " Prefers item with matched substring closer to the end"" +.br +.BR index " Prefers item that appeared earlier in the input stream" +.br .SS Interface .TP .B "-m, --multi" diff --git a/src/core.go b/src/core.go index 9f33b41..4a83424 100644 --- a/src/core.go +++ b/src/core.go @@ -55,6 +55,7 @@ func Run(options *Options) { opts := ParseOptions() sort := opts.Sort > 0 + rankTiebreak = opts.Tiebreak if opts.Version { fmt.Println(Version) diff --git a/src/item.go b/src/item.go index 9e2e1e7..996c5e1 100644 --- a/src/item.go +++ b/src/item.go @@ -1,6 +1,8 @@ package fzf import ( + "math" + "github.com/junegunn/fzf/src/curses" ) @@ -27,17 +29,21 @@ type Item struct { // Rank is used to sort the search result type Rank struct { matchlen uint16 - strlen uint16 + tiebreak uint16 index uint32 } +// Tiebreak criterion to use. Never changes once fzf is started. +var rankTiebreak tiebreak + // Rank calculates rank of the Item func (i *Item) Rank(cache bool) Rank { - if cache && (i.rank.matchlen > 0 || i.rank.strlen > 0) { + if cache && (i.rank.matchlen > 0 || i.rank.tiebreak > 0) { return i.rank } matchlen := 0 prevEnd := 0 + minBegin := math.MaxUint16 for _, offset := range i.offsets { begin := int(offset[0]) end := int(offset[1]) @@ -48,10 +54,30 @@ func (i *Item) Rank(cache bool) Rank { prevEnd = end } if end > begin { + if begin < minBegin { + minBegin = begin + } matchlen += end - begin } } - rank := Rank{uint16(matchlen), uint16(len(*i.text)), i.index} + var tiebreak uint16 + switch rankTiebreak { + case byLength: + tiebreak = uint16(len(*i.text)) + case byBegin: + // We can't just look at i.offsets[0][0] because it can be an inverse term + tiebreak = uint16(minBegin) + case byEnd: + if prevEnd > 0 { + tiebreak = uint16(1 + len(*i.text) - prevEnd) + } else { + // Empty offsets due to inverse terms. + tiebreak = 1 + } + case byIndex: + tiebreak = 1 + } + rank := Rank{uint16(matchlen), tiebreak, i.index} if cache { i.rank = rank } @@ -199,9 +225,9 @@ func compareRanks(irank Rank, jrank Rank, tac bool) bool { return false } - if irank.strlen < jrank.strlen { + if irank.tiebreak < jrank.tiebreak { return true - } else if irank.strlen > jrank.strlen { + } else if irank.tiebreak > jrank.tiebreak { return false } diff --git a/src/item_test.go b/src/item_test.go index 4eea8c1..2d375e4 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -42,7 +42,7 @@ func TestItemRank(t *testing.T) { strs := []string{"foo", "foobar", "bar", "baz"} item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}} rank1 := item1.Rank(true) - if rank1.matchlen != 0 || rank1.strlen != 3 || rank1.index != 1 { + if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 { t.Error(item1.Rank(true)) } // Only differ in index diff --git a/src/options.go b/src/options.go index c186542..d13a53b 100644 --- a/src/options.go +++ b/src/options.go @@ -28,7 +28,8 @@ const usage = `usage: fzf [options] Search result +s, --no-sort Do not sort the result --tac Reverse the order of the input - (e.g. 'history | fzf --tac --no-sort') + --tiebreak=CRI Sort criterion when the scores are tied; + [length|begin|end|index] (default: length) Interface -m, --multi Enable multi-select with tab/shift-tab @@ -50,7 +51,6 @@ const usage = `usage: fzf [options] --expect=KEYS Comma-separated list of keys to complete fzf --toggle-sort=KEY Key to toggle sort --sync Synchronous search for multi-staged filtering - (e.g. 'fzf --multi | fzf --sync') Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty @@ -78,6 +78,16 @@ const ( CaseRespect ) +// Sort criteria +type tiebreak int + +const ( + byLength tiebreak = iota + byBegin + byEnd + byIndex +) + // Options stores the values of command-line options type Options struct { Mode Mode @@ -87,6 +97,7 @@ type Options struct { Delimiter *regexp.Regexp Sort int Tac bool + Tiebreak tiebreak Multi bool Ansi bool Mouse bool @@ -116,6 +127,7 @@ func defaultOptions() *Options { Delimiter: nil, Sort: 1000, Tac: false, + Tiebreak: byLength, Multi: false, Ansi: false, Mouse: true, @@ -238,6 +250,22 @@ func parseKeyChords(str string, message string) []int { return chords } +func parseTiebreak(str string) tiebreak { + switch strings.ToLower(str) { + case "length": + return byLength + case "index": + return byIndex + case "begin": + return byBegin + case "end": + return byEnd + default: + errorExit("invalid sort criterion: " + str) + } + return byLength +} + func checkToggleSort(str string) int { keys := parseKeyChords(str, "key name required") if len(keys) != 1 { @@ -265,6 +293,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Filter = &filter case "--expect": opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") + case "--tiebreak": + opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--toggle-sort": opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required")) case "-d", "--delimiter": @@ -352,6 +382,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.ToggleSort = checkToggleSort(value) } else if match, value := optString(arg, "--expect="); match { opts.Expect = parseKeyChords(value, "key names required") + } else if match, value := optString(arg, "--tiebreak="); match { + opts.Tiebreak = parseTiebreak(value) } else { errorExit("unknown option: " + arg) } diff --git a/test/test_go.rb b/test/test_go.rb index a9284b6..abf0496 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -476,19 +476,66 @@ class TestGoFZF < TestBase def test_unicode_case tempname = TEMPNAME + Time.now.to_f.to_s - File.open(tempname, 'w') do |f| - f << %w[строКА1 СТРОКА2 строка3 Строка4].join($/) - f.sync - end - since = Time.now - while `cat #{tempname}`.split($/).length != 4 && (Time.now - since) < 10 - sleep 0.1 - end + writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4] assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/) assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/) rescue File.unlink tempname end + + def test_tiebreak + tempname = TEMPNAME + Time.now.to_f.to_s + input = %w[ + --foobar-------- + -----foobar--- + ----foobar-- + -------foobar- + ] + writelines tempname, input + + assert_equal input, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=index`.split($/) + + by_length = %w[ + ----foobar-- + -----foobar--- + -------foobar- + --foobar-------- + ] + assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar`.split($/) + assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=length`.split($/) + + by_begin = %w[ + --foobar-------- + ----foobar-- + -----foobar--- + -------foobar- + ] + assert_equal by_begin, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=begin`.split($/) + assert_equal by_begin, `cat #{tempname} | #{FZF} -f"!z foobar" -x --tiebreak begin`.split($/) + + assert_equal %w[ + -------foobar- + ----foobar-- + -----foobar--- + --foobar-------- + ], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/) + + assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/) + rescue + File.unlink tempname + end + +private + def writelines path, lines, timeout = 10 + File.open(path, 'w') do |f| + f << lines.join($/) + f.sync + end + since = Time.now + while `cat #{path}`.split($/).length != lines.length && (Time.now - since) < 10 + sleep 0.1 + end + end end module TestShell From d08542ce5d4a81c13404311c3d1605654978bd1b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 14:20:29 +0900 Subject: [PATCH 194/408] Prepare for 0.9.9 release --- CHANGELOG.md | 8 ++++++++ install | 2 +- shell/completion.bash | 14 +++++++++++++- shell/key-bindings.bash | 4 ++-- shell/key-bindings.fish | 2 +- shell/key-bindings.zsh | 2 +- src/constants.go | 2 +- 7 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fd89e5..c3399fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +0.9.9 +----- + +### New features + +- Added `--tiebreak` option (#191) +- Added `--no-hscroll` option (#193) + 0.9.8 ----- diff --git a/install b/install index c47b6ec..c27470c 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.8 +version=0.9.9 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/shell/completion.bash b/shell/completion.bash index 04287a6..62dc140 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -16,9 +16,10 @@ _fzf_orig_completion_filter() { } _fzf_opts_completion() { - local cur opts + local cur prev opts COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" opts=" -x --extended -e --extended-exact @@ -27,20 +28,31 @@ _fzf_opts_completion() { -d --delimiter +s --no-sort --tac + --tiebreak -m --multi --no-mouse +c --no-color +2 --no-256 --black --reverse + --no-hscroll --prompt -q --query -1 --select-1 -0 --exit-0 -f --filter --print-query + --expect + --toggle-sort --sync" + case "${prev}" in + --tiebreak) + COMPREPLY=( $(compgen -W "length begin end index" -- ${cur}) ) + return 0 + ;; + esac + if [[ ${cur} =~ ^-|\+ ]]; then COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index d7f0903..d983bb1 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -44,7 +44,7 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then fi # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. --toggle-sort=ctrl-r | sed \"s/ *[0-9]* *//\")\e\C-e\er"' + bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed \"s/ *[0-9]* *//\")\e\C-e\er"' # ALT-C - cd into the selected directory bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' @@ -62,7 +62,7 @@ else bind -m vi-command '"\C-t": "i\C-t"' # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' + bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' bind -m vi-command '"\C-r": "i\C-r"' # ALT-C - cd into the selected directory diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 6e9efa4..f5f79c4 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -44,7 +44,7 @@ function fzf_key_bindings end function __fzf_ctrl_r - history | fzf +s +m --toggle-sort=ctrl-r > $TMPDIR/fzf.result + history | fzf +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result and commandline (cat $TMPDIR/fzf.result) commandline -f repaint rm -f $TMPDIR/fzf.result diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 4780658..2bf42fa 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -45,7 +45,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected - if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. --toggle-sort=ctrl-r -q "$LBUFFER"); then + if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') LBUFFER=!$num zle expand-history diff --git a/src/constants.go b/src/constants.go index 36804d0..3c7f76c 100644 --- a/src/constants.go +++ b/src/constants.go @@ -5,7 +5,7 @@ import ( ) // Current version -const Version = "0.9.8" +const Version = "0.9.9" // fzf events const ( From d1d59272a2e148e936daff9cefb23cfcaf408848 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 14:44:41 +0900 Subject: [PATCH 195/408] Add visual indication of --toggle-sort Close #194 --- CHANGELOG.md | 1 + src/core.go | 5 +++-- src/terminal.go | 12 +++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3399fd..e3c58dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG - Added `--tiebreak` option (#191) - Added `--no-hscroll` option (#193) +- Visual indication of `--toggle-sort` (#194) 0.9.8 ----- diff --git a/src/core.go b/src/core.go index 4a83424..b56fbf6 100644 --- a/src/core.go +++ b/src/core.go @@ -195,8 +195,9 @@ func Run(options *Options) { matcher.Reset(snapshot, terminal.Input(), false, !reading, sort) case EvtSearchNew: - if value.(bool) { - sort = !sort + switch val := value.(type) { + case bool: + sort = val } snapshot, _ := chunkList.Snapshot() matcher.Reset(snapshot, terminal.Input(), true, !reading, sort) diff --git a/src/terminal.go b/src/terminal.go index b16daec..790ccc1 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -29,6 +29,7 @@ type Terminal struct { yanked []rune input []rune multi bool + sort bool toggleSort int expect []int pressed int @@ -96,6 +97,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { yanked: []rune{}, input: input, multi: opts.Multi, + sort: opts.Sort > 0, toggleSort: opts.ToggleSort, expect: opts.Expect, pressed: 0, @@ -241,6 +243,13 @@ func (t *Terminal) printInfo() { t.move(1, 2, false) output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) + if t.toggleSort > 0 { + if t.sort { + output += "/S" + } else { + output += " " + } + } if t.multi && len(t.selected) > 0 { output += fmt.Sprintf(" (%d)", len(t.selected)) } @@ -579,7 +588,8 @@ func (t *Terminal) Loop() { } if t.toggleSort > 0 { if keyMatch(t.toggleSort, event) { - t.eventBox.Set(EvtSearchNew, true) + t.sort = !t.sort + t.eventBox.Set(EvtSearchNew, t.sort) t.mutex.Unlock() continue } From 2db2feea379af344c7289afbd04970e9d6f22a4a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 21:58:41 +0900 Subject: [PATCH 196/408] `install --bin` just for downloading the binary --- install | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install b/install index c27470c..038e1f3 100755 --- a/install +++ b/install @@ -161,6 +161,8 @@ if [ -n "$binary_error" ]; then echo "OK" fi +[[ "$*" =~ "--bin" ]] && exit 0 + # Auto-completion ask "Do you want to add auto-completion support?" auto_completion=$? From f9225f98e7a4ce1678956b0263ec3b994ada50dd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 22:13:31 +0900 Subject: [PATCH 197/408] Fix sort control from Terminal --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 790ccc1..667d206 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -723,7 +723,7 @@ func (t *Terminal) Loop() { t.mutex.Unlock() // Must be unlocked before touching reqBox if changed { - t.eventBox.Set(EvtSearchNew, false) + t.eventBox.Set(EvtSearchNew, t.sort) } for _, event := range events { t.reqBox.Set(event, nil) From a30908c66a32b57ad91cd1f5c993b8017c0699a5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 22:24:12 +0900 Subject: [PATCH 198/408] [vim] Automatically download Go binary when not found --- plugin/fzf.vim | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 99aa300..7015e28 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -24,6 +24,8 @@ let s:default_height = '40%' let s:launcher = 'xterm -e bash -ic %s' let s:fzf_go = expand(':h:h').'/bin/fzf' +let s:install = expand(':h:h').'/install' +let s:installed = 0 let s:fzf_rb = expand(':h:h').'/fzf' let s:fzf_tmux = expand(':h:h').'/bin/fzf-tmux' @@ -34,6 +36,13 @@ function! s:fzf_exec() if !exists('s:exec') if executable(s:fzf_go) let s:exec = s:fzf_go + elseif !s:installed && executable(s:install) + echohl WarningMsg + echo 'Downloading fzf binary. Please wait ...' + echohl None + let s:installed = 1 + call system(s:install.' --bin') + return s:fzf_exec() else let path = split(system('which fzf 2> /dev/null'), '\n') if !v:shell_error && !empty(path) @@ -99,7 +108,7 @@ function! fzf#run(...) abort if has('nvim') && bufexists('[FZF]') echohl WarningMsg echomsg 'FZF is already running!' - echohl NONE + echohl None return [] endif let dict = exists('a:1') ? s:upgrade(a:1) : {} From f77ed0fb07f59cc5972a7377da1dd44751dece32 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 22:34:02 +0900 Subject: [PATCH 199/408] Fix typo in man page --- man/man1/fzf.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index ada0340..a16dd20 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -76,7 +76,7 @@ Sort criterion to use when the scores are tied .br .BR begin " Prefers item with matched substring closer to the beginning" .br -.BR end " Prefers item with matched substring closer to the end"" +.BR end " Prefers item with matched substring closer to the end" .br .BR index " Prefers item that appeared earlier in the input stream" .br From cc67d2e1cfd6bea9c09dec0816e9ca700aa87a14 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 22:39:51 +0900 Subject: [PATCH 200/408] Test case for visual indicator of --toggle sort (#194) --- test/test_go.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index abf0496..3a9f1fd 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -465,11 +465,11 @@ class TestGoFZF < TestBase tmux.send_keys "seq 1 111 | #{fzf '-m +s --tac --toggle-sort=ctrl-r -q11'}", :Enter tmux.until { |lines| lines[-3].include? '> 111' } tmux.send_keys :Tab - tmux.until { |lines| lines[-2].include? '4/111 (1)' } + tmux.until { |lines| lines[-2].include? '4/111 (1)' } tmux.send_keys 'C-R' tmux.until { |lines| lines[-3].include? '> 11' } tmux.send_keys :Tab - tmux.until { |lines| lines[-2].include? '4/111 (2)' } + tmux.until { |lines| lines[-2].include? '4/111/S (2)' } tmux.send_keys :Enter assert_equal ['111', '11'], readonce.split($/) end From 3610acec5a36231a814c5c6f35aa2dabd8f83847 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 22:52:02 +0900 Subject: [PATCH 201/408] 0.9.9 --- man/man1/fzf.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index a16dd20..664f901 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "March 2015" "fzf 0.9.6" "fzf - a command-line fuzzy finder" +.TH fzf 1 "April 2015" "fzf 0.9.9" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder From 288131ac5a895ba335681339d85ee039557490da Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Apr 2015 23:11:11 +0900 Subject: [PATCH 202/408] Update man page to be consistent with --help --- man/man1/fzf.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 664f901..cff8160 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -67,7 +67,7 @@ Reverse the order of the input e.g. \fBhistory | fzf --tac --no-sort\fR .RE .TP -.BI "--tiebreak=" "STR" +.BI "--tiebreak=" "CRI" Sort criterion to use when the scores are tied .br .R "" From 2fe1e28220c543ddbf4e12ee7396e44ee85ad8e0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 17 Apr 2015 22:23:52 +0900 Subject: [PATCH 203/408] Improvements in performance and memory usage I profiled fzf and it turned out that it was spending significant amount of time repeatedly converting character arrays into Unicode codepoints. This commit greatly improves search performance after the initial scan by memoizing the converted results. This commit also addresses the problem of unbounded memory usage of fzf. fzf is a short-lived process that usually processes small input, so it was implemented to cache the intermediate results very aggressively with no notion of cache expiration/eviction. I still think a proper implementation of caching scheme is definitely an overkill. Instead this commit introduces limits to the maximum size (or minimum selectivity) of the intermediate results that can be cached. --- CHANGELOG.md | 8 +++++++ src/algo/algo.go | 54 ++++++++++++------------------------------- src/algo/algo_test.go | 6 ++--- src/cache.go | 12 +++++----- src/cache_test.go | 2 +- src/chunklist.go | 11 ++++----- src/chunklist_test.go | 4 ++-- src/constants.go | 31 +++++++++++++++++++++++-- src/core.go | 6 ++--- src/item.go | 2 +- src/matcher.go | 8 +++---- src/merger.go | 8 +++++-- src/pattern.go | 19 ++++++++------- src/pattern_test.go | 4 ++-- src/reader.go | 2 -- src/terminal.go | 17 +++++--------- src/tokenizer.go | 44 +++++++++++++++++------------------ src/tokenizer_test.go | 30 ++++++++++++------------ src/util/util.go | 11 +++++++++ 19 files changed, 144 insertions(+), 135 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3c58dd..d87876d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +0.9.10 +------ + +### Improvements + +- Performance optimization +- Less aggressive memoization to limit memory usage + 0.9.9 ----- diff --git a/src/algo/algo.go b/src/algo/algo.go index 36c8d87..c1c07f3 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -1,8 +1,9 @@ package algo import ( - "strings" "unicode" + + "github.com/junegunn/fzf/src/util" ) /* @@ -14,13 +15,11 @@ import ( */ // FuzzyMatch performs fuzzy-match -func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { +func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { if len(pattern) == 0 { return 0, 0 } - runes := []rune(*input) - // 0. (FIXME) How to find the shortest match? // a_____b__c__abc // ^^^^^^^^^^ ^^^ @@ -34,7 +33,7 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { sidx := -1 eidx := -1 - for index, char := range runes { + for index, char := range *runes { // This is considerably faster than blindly applying strings.ToLower to the // whole string if !caseSensitive { @@ -43,10 +42,10 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { // compiler as of now does not inline non-leaf functions.) if char >= 'A' && char <= 'Z' { char += 32 - runes[index] = char + (*runes)[index] = char } else if char > unicode.MaxASCII { char = unicode.To(unicode.LowerCase, char) - runes[index] = char + (*runes)[index] = char } } if char == pattern[pidx] { @@ -63,7 +62,7 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { if sidx >= 0 && eidx >= 0 { pidx-- for index := eidx - 1; index >= sidx; index-- { - char := runes[index] + char := (*runes)[index] if char == pattern[pidx] { if pidx--; pidx < 0 { sidx = index @@ -76,27 +75,6 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { return -1, -1 } -// ExactMatchStrings performs exact-match using strings package. -// Currently not used. -func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, int) { - if len(pattern) == 0 { - return 0, 0 - } - - var str string - if caseSensitive { - str = *input - } else { - str = strings.ToLower(*input) - } - - if idx := strings.Index(str, string(pattern)); idx >= 0 { - prefixRuneLen := len([]rune((*input)[:idx])) - return prefixRuneLen, prefixRuneLen + len(pattern) - } - return -1, -1 -} - // ExactMatchNaive is a basic string searching algorithm that handles case // sensitivity. Although naive, it still performs better than the combination // of strings.ToLower + strings.Index for typical fzf use cases where input @@ -104,13 +82,12 @@ func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, // // We might try to implement better algorithms in the future: // http://en.wikipedia.org/wiki/String_searching_algorithm -func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, int) { +func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { if len(pattern) == 0 { return 0, 0 } - runes := []rune(*input) - numRunes := len(runes) + numRunes := len(*runes) plen := len(pattern) if numRunes < plen { return -1, -1 @@ -118,7 +95,7 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in pidx := 0 for index := 0; index < numRunes; index++ { - char := runes[index] + char := (*runes)[index] if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -140,14 +117,13 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in } // PrefixMatch performs prefix-match -func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { - runes := []rune(*input) - if len(runes) < len(pattern) { +func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { + if len(*runes) < len(pattern) { return -1, -1 } for index, r := range pattern { - char := runes[index] + char := (*runes)[index] if !caseSensitive { char = unicode.ToLower(char) } @@ -159,8 +135,8 @@ func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { } // SuffixMatch performs suffix-match -func SuffixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) { - runes := []rune(strings.TrimRight(*input, " ")) +func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) { + runes := util.TrimRight(input) trimmedLen := len(runes) diff := trimmedLen - len(pattern) if diff < 0 { diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index ac7aad5..32056df 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -5,11 +5,12 @@ import ( "testing" ) -func assertMatch(t *testing.T, fun func(bool, *string, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { +func assertMatch(t *testing.T, fun func(bool, *[]rune, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { if !caseSensitive { pattern = strings.ToLower(pattern) } - s, e := fun(caseSensitive, &input, []rune(pattern)) + runes := []rune(input) + s, e := fun(caseSensitive, &runes, []rune(pattern)) if s != sidx { t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) } @@ -45,7 +46,6 @@ func TestSuffixMatch(t *testing.T) { func TestEmptyPattern(t *testing.T) { assertMatch(t, FuzzyMatch, true, "foobar", "", 0, 0) - assertMatch(t, ExactMatchStrings, true, "foobar", "", 0, 0) assertMatch(t, ExactMatchNaive, true, "foobar", "", 0, 0) assertMatch(t, PrefixMatch, true, "foobar", "", 0, 0) assertMatch(t, SuffixMatch, true, "foobar", "", 6, 6) diff --git a/src/cache.go b/src/cache.go index f2f84a0..d2ec00b 100644 --- a/src/cache.go +++ b/src/cache.go @@ -2,23 +2,23 @@ package fzf import "sync" -// QueryCache associates strings to lists of items -type QueryCache map[string][]*Item +// queryCache associates strings to lists of items +type queryCache map[string][]*Item // ChunkCache associates Chunk and query string to lists of items type ChunkCache struct { mutex sync.Mutex - cache map[*Chunk]*QueryCache + cache map[*Chunk]*queryCache } // NewChunkCache returns a new ChunkCache func NewChunkCache() ChunkCache { - return ChunkCache{sync.Mutex{}, make(map[*Chunk]*QueryCache)} + return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)} } // Add adds the list to the cache func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { - if len(key) == 0 || !chunk.IsFull() { + if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax { return } @@ -27,7 +27,7 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { qc, ok := cc.cache[chunk] if !ok { - cc.cache[chunk] = &QueryCache{} + cc.cache[chunk] = &queryCache{} qc = cc.cache[chunk] } (*qc)[key] = list diff --git a/src/cache_test.go b/src/cache_test.go index 3975eaa..05f904c 100644 --- a/src/cache_test.go +++ b/src/cache_test.go @@ -4,7 +4,7 @@ import "testing" func TestChunkCache(t *testing.T) { cache := NewChunkCache() - chunk2 := make(Chunk, ChunkSize) + chunk2 := make(Chunk, chunkSize) chunk1p := &Chunk{} chunk2p := &chunk2 items1 := []*Item{&Item{}} diff --git a/src/chunklist.go b/src/chunklist.go index 571a59a..52084f2 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -2,10 +2,7 @@ package fzf import "sync" -// Capacity of each chunk -const ChunkSize int = 100 - -// Chunk is a list of Item pointers whose size has the upper limit of ChunkSize +// Chunk is a list of Item pointers whose size has the upper limit of chunkSize type Chunk []*Item // >>> []Item // ItemBuilder is a closure type that builds Item object from a pointer to a @@ -35,7 +32,7 @@ func (c *Chunk) push(trans ItemBuilder, data *string, index int) { // IsFull returns true if the Chunk is full func (c *Chunk) IsFull() bool { - return len(*c) == ChunkSize + return len(*c) == chunkSize } func (cl *ChunkList) lastChunk() *Chunk { @@ -47,7 +44,7 @@ func CountItems(cs []*Chunk) int { if len(cs) == 0 { return 0 } - return ChunkSize*(len(cs)-1) + len(*(cs[len(cs)-1])) + return chunkSize*(len(cs)-1) + len(*(cs[len(cs)-1])) } // Push adds the item to the list @@ -56,7 +53,7 @@ func (cl *ChunkList) Push(data string) { defer cl.mutex.Unlock() if len(cl.chunks) == 0 || cl.lastChunk().IsFull() { - newChunk := Chunk(make([]*Item, 0, ChunkSize)) + newChunk := Chunk(make([]*Item, 0, chunkSize)) cl.chunks = append(cl.chunks, &newChunk) } diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 02288d9..2f8ef7e 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -45,7 +45,7 @@ func TestChunkList(t *testing.T) { } // Add more data - for i := 0; i < ChunkSize*2; i++ { + for i := 0; i < chunkSize*2; i++ { cl.Push(fmt.Sprintf("item %d", i)) } @@ -57,7 +57,7 @@ func TestChunkList(t *testing.T) { // New snapshot snapshot, count = cl.Snapshot() if len(snapshot) != 3 || !snapshot[0].IsFull() || - !snapshot[1].IsFull() || snapshot[2].IsFull() || count != ChunkSize*2+2 { + !snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 { t.Error("Expected two full chunks and one more chunk") } if len(*snapshot[2]) != 2 { diff --git a/src/constants.go b/src/constants.go index 3c7f76c..07ac752 100644 --- a/src/constants.go +++ b/src/constants.go @@ -1,11 +1,38 @@ package fzf import ( + "time" + "github.com/junegunn/fzf/src/util" ) -// Current version -const Version = "0.9.9" +const ( + // Current version + Version = "0.9.9" + + // Core + coordinatorDelayMax time.Duration = 100 * time.Millisecond + coordinatorDelayStep time.Duration = 10 * time.Millisecond + + // Reader + defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` + + // Terminal + initialDelay = 100 * time.Millisecond + spinnerDuration = 200 * time.Millisecond + + // Matcher + progressMinDuration = 200 * time.Millisecond + + // Capacity of each chunk + chunkSize int = 100 + + // Do not cache results of low selectivity queries + queryCacheMax int = chunkSize / 5 + + // Not to cache mergers with large lists + mergerCacheMax int = 100000 +) // fzf events const ( diff --git a/src/core.go b/src/core.go index b56fbf6..7230cff 100644 --- a/src/core.go +++ b/src/core.go @@ -34,9 +34,6 @@ import ( "github.com/junegunn/fzf/src/util" ) -const coordinatorDelayMax time.Duration = 100 * time.Millisecond -const coordinatorDelayStep time.Duration = 10 * time.Millisecond - func initProcs() { runtime.GOMAXPROCS(runtime.NumCPU()) } @@ -99,8 +96,9 @@ func Run(options *Options) { } else { chunkList = NewChunkList(func(data *string, index int) *Item { tokens := Tokenize(data, opts.Delimiter) + trans := Transform(tokens, opts.WithNth) item := Item{ - text: Transform(tokens, opts.WithNth).whole, + text: joinTokens(trans), origText: data, index: uint32(index), colors: nil, diff --git a/src/item.go b/src/item.go index 996c5e1..711adbe 100644 --- a/src/item.go +++ b/src/item.go @@ -19,7 +19,7 @@ type colorOffset struct { type Item struct { text *string origText *string - transformed *Transformed + transformed *[]Token index uint32 offsets []Offset colors []ansiOffset diff --git a/src/matcher.go b/src/matcher.go index 0f3b409..d01ed23 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -34,10 +34,6 @@ const ( reqReset ) -const ( - progressMinDuration = 200 * time.Millisecond -) - // NewMatcher returns a new Matcher func NewMatcher(patternBuilder func([]rune) *Pattern, sort bool, tac bool, eventBox *util.EventBox) *Matcher { @@ -100,7 +96,9 @@ func (m *Matcher) Loop() { } if !cancelled { - m.mergerCache[patternString] = merger + if merger.Cacheable() { + m.mergerCache[patternString] = merger + } merger.final = request.final m.eventBox.Set(EvtSearchFin, merger) } diff --git a/src/merger.go b/src/merger.go index bc5d5de..4c7966a 100644 --- a/src/merger.go +++ b/src/merger.go @@ -61,8 +61,8 @@ func (mg *Merger) Get(idx int) *Item { if mg.tac { idx = mg.count - idx - 1 } - chunk := (*mg.chunks)[idx/ChunkSize] - return (*chunk)[idx%ChunkSize] + chunk := (*mg.chunks)[idx/chunkSize] + return (*chunk)[idx%chunkSize] } if mg.sorted { @@ -82,6 +82,10 @@ func (mg *Merger) Get(idx int) *Item { panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) } +func (mg *Merger) Cacheable() bool { + return mg.count < mergerCacheMax +} + func (mg *Merger) mergedGet(idx int) *Item { for i := len(mg.merged); i <= idx; i++ { minRank := Rank{0, 0, 0} diff --git a/src/pattern.go b/src/pattern.go index e6bda5f..75cc5f8 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -43,7 +43,7 @@ type Pattern struct { hasInvTerm bool delimiter *regexp.Regexp nth []Range - procFun map[termType]func(bool, *string, []rune) (int, int) + procFun map[termType]func(bool, *[]rune, []rune) (int, int) } var ( @@ -122,7 +122,7 @@ func BuildPattern(mode Mode, caseMode Case, hasInvTerm: hasInvTerm, nth: nth, delimiter: delimiter, - procFun: make(map[termType]func(bool, *string, []rune) (int, int))} + procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))} ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termExact] = algo.ExactMatchNaive @@ -300,28 +300,27 @@ func (p *Pattern) extendedMatch(item *Item) []Offset { return offsets } -func (p *Pattern) prepareInput(item *Item) *Transformed { +func (p *Pattern) prepareInput(item *Item) *[]Token { if item.transformed != nil { return item.transformed } - var ret *Transformed + var ret *[]Token if len(p.nth) > 0 { tokens := Tokenize(item.text, p.delimiter) ret = Transform(tokens, p.nth) } else { - trans := Transformed{ - whole: item.text, - parts: []Token{Token{text: item.text, prefixLength: 0}}} + runes := []rune(*item.text) + trans := []Token{Token{text: &runes, prefixLength: 0}} ret = &trans } item.transformed = ret return ret } -func (p *Pattern) iter(pfun func(bool, *string, []rune) (int, int), - inputs *Transformed, pattern []rune) (int, int) { - for _, part := range inputs.parts { +func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int), + tokens *[]Token, pattern []rune) (int, int) { + for _, part := range *tokens { prefixLength := part.prefixLength if sidx, eidx := pfun(p.caseSensitive, part.text, pattern); sidx >= 0 { return sidx + prefixLength, eidx + prefixLength diff --git a/src/pattern_test.go b/src/pattern_test.go index 67542f2..c73e650 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -58,8 +58,8 @@ func TestExact(t *testing.T) { clearPatternCache() pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("'abc")) - str := "aabbcc abc" - sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text) + runes := []rune("aabbcc abc") + sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &runes, pattern.terms[0].text) if sidx != 7 || eidx != 10 { t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) } diff --git a/src/reader.go b/src/reader.go index 2c10b8a..d476411 100644 --- a/src/reader.go +++ b/src/reader.go @@ -9,8 +9,6 @@ import ( "github.com/junegunn/fzf/src/util" ) -const defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` - // Reader reads from command or standard input type Reader struct { pusher func(string) diff --git a/src/terminal.go b/src/terminal.go index 667d206..eaec9a3 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -38,7 +38,7 @@ type Terminal struct { progress int reading bool merger *Merger - selected map[*string]selectedItem + selected map[uint32]selectedItem reqBox *util.EventBox eventBox *util.EventBox mutex sync.Mutex @@ -79,11 +79,6 @@ const ( reqQuit ) -const ( - initialDelay = 100 * time.Millisecond - spinnerDuration = 200 * time.Millisecond -) - // NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) @@ -103,7 +98,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { pressed: 0, printQuery: opts.PrintQuery, merger: EmptyMerger, - selected: make(map[*string]selectedItem), + selected: make(map[uint32]selectedItem), reqBox: util.NewEventBox(), eventBox: eventBox, mutex: sync.Mutex{}, @@ -273,7 +268,7 @@ func (t *Terminal) printList() { } func (t *Terminal) printItem(item *Item, current bool) { - _, selected := t.selected[item.text] + _, selected := t.selected[item.index] if current { C.CPrint(C.ColCursor, true, ">") if selected { @@ -565,16 +560,16 @@ func (t *Terminal) Loop() { toggle := func() { if t.cy < t.merger.Length() { item := t.merger.Get(t.cy) - if _, found := t.selected[item.text]; !found { + if _, found := t.selected[item.index]; !found { var strptr *string if item.origText != nil { strptr = item.origText } else { strptr = item.text } - t.selected[item.text] = selectedItem{time.Now(), strptr} + t.selected[item.index] = selectedItem{time.Now(), strptr} } else { - delete(t.selected, item.text) + delete(t.selected, item.index) } req(reqInfo) } diff --git a/src/tokenizer.go b/src/tokenizer.go index d38f46f..c61b238 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -16,15 +16,9 @@ type Range struct { end int } -// Transformed holds the result of tokenization and transformation -type Transformed struct { - whole *string - parts []Token -} - // Token contains the tokenized part of the strings and its prefix length type Token struct { - text *string + text *[]rune prefixLength int } @@ -81,8 +75,8 @@ func withPrefixLengths(tokens []string, begin int) []Token { for idx, token := range tokens { // Need to define a new local variable instead of the reused token to take // the pointer to it - str := token - ret[idx] = Token{text: &str, prefixLength: prefixLength} + runes := []rune(token) + ret[idx] = Token{text: &runes, prefixLength: prefixLength} prefixLength += len([]rune(token)) } return ret @@ -142,33 +136,40 @@ func Tokenize(str *string, delimiter *regexp.Regexp) []Token { return withPrefixLengths(tokens, 0) } -func joinTokens(tokens []Token) string { +func joinTokens(tokens *[]Token) *string { ret := "" - for _, token := range tokens { - ret += *token.text + for _, token := range *tokens { + ret += string(*token.text) } - return ret + return &ret +} + +func joinTokensAsRunes(tokens *[]Token) *[]rune { + ret := []rune{} + for _, token := range *tokens { + ret = append(ret, *token.text...) + } + return &ret } // Transform is used to transform the input when --with-nth option is given -func Transform(tokens []Token, withNth []Range) *Transformed { +func Transform(tokens []Token, withNth []Range) *[]Token { transTokens := make([]Token, len(withNth)) numTokens := len(tokens) - whole := "" for idx, r := range withNth { - part := "" + part := []rune{} minIdx := 0 if r.begin == r.end { idx := r.begin if idx == rangeEllipsis { - part += joinTokens(tokens) + part = append(part, *joinTokensAsRunes(&tokens)...) } else { if idx < 0 { idx += numTokens + 1 } if idx >= 1 && idx <= numTokens { minIdx = idx - 1 - part += *tokens[idx-1].text + part = append(part, *tokens[idx-1].text...) } } } else { @@ -195,11 +196,10 @@ func Transform(tokens []Token, withNth []Range) *Transformed { minIdx = util.Max(0, begin-1) for idx := begin; idx <= end; idx++ { if idx >= 1 && idx <= numTokens { - part += *tokens[idx-1].text + part = append(part, *tokens[idx-1].text...) } } } - whole += part var prefixLength int if minIdx < numTokens { prefixLength = tokens[minIdx].prefixLength @@ -208,7 +208,5 @@ func Transform(tokens []Token, withNth []Range) *Transformed { } transTokens[idx] = Token{&part, prefixLength} } - return &Transformed{ - whole: &whole, - parts: transTokens} + return &transTokens } diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index 5195a1b..0362b5a 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -44,13 +44,13 @@ func TestTokenize(t *testing.T) { // AWK-style input := " abc: def: ghi " tokens := Tokenize(&input, nil) - if *tokens[0].text != "abc: " || tokens[0].prefixLength != 2 { + if string(*tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 { t.Errorf("%s", tokens) } // With delimiter tokens = Tokenize(&input, delimiterRegexp(":")) - if *tokens[0].text != " abc:" || tokens[0].prefixLength != 0 { + if string(*tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 { t.Errorf("%s", tokens) } } @@ -62,19 +62,19 @@ func TestTransform(t *testing.T) { { ranges := splitNth("1,2,3") tx := Transform(tokens, ranges) - if *tx.whole != "abc: def: ghi: " { + if *joinTokens(tx) != "abc: def: ghi: " { t.Errorf("%s", *tx) } } { ranges := splitNth("1..2,3,2..,1") tx := Transform(tokens, ranges) - if *tx.whole != "abc: def: ghi: def: ghi: jklabc: " || - len(tx.parts) != 4 || - *tx.parts[0].text != "abc: def: " || tx.parts[0].prefixLength != 2 || - *tx.parts[1].text != "ghi: " || tx.parts[1].prefixLength != 14 || - *tx.parts[2].text != "def: ghi: jkl" || tx.parts[2].prefixLength != 8 || - *tx.parts[3].text != "abc: " || tx.parts[3].prefixLength != 2 { + if *joinTokens(tx) != "abc: def: ghi: def: ghi: jklabc: " || + len(*tx) != 4 || + string(*(*tx)[0].text) != "abc: def: " || (*tx)[0].prefixLength != 2 || + string(*(*tx)[1].text) != "ghi: " || (*tx)[1].prefixLength != 14 || + string(*(*tx)[2].text) != "def: ghi: jkl" || (*tx)[2].prefixLength != 8 || + string(*(*tx)[3].text) != "abc: " || (*tx)[3].prefixLength != 2 { t.Errorf("%s", *tx) } } @@ -84,12 +84,12 @@ func TestTransform(t *testing.T) { { ranges := splitNth("1..2,3,2..,1") tx := Transform(tokens, ranges) - if *tx.whole != " abc: def: ghi: def: ghi: jkl abc:" || - len(tx.parts) != 4 || - *tx.parts[0].text != " abc: def:" || tx.parts[0].prefixLength != 0 || - *tx.parts[1].text != " ghi:" || tx.parts[1].prefixLength != 12 || - *tx.parts[2].text != " def: ghi: jkl" || tx.parts[2].prefixLength != 6 || - *tx.parts[3].text != " abc:" || tx.parts[3].prefixLength != 0 { + if *joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || + len(*tx) != 4 || + string(*(*tx)[0].text) != " abc: def:" || (*tx)[0].prefixLength != 0 || + string(*(*tx)[1].text) != " ghi:" || (*tx)[1].prefixLength != 12 || + string(*(*tx)[2].text) != " def: ghi: jkl" || (*tx)[2].prefixLength != 6 || + string(*(*tx)[3].text) != " abc:" || (*tx)[3].prefixLength != 0 { t.Errorf("%s", *tx) } } diff --git a/src/util/util.go b/src/util/util.go index 931b14a..511de1e 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -77,3 +77,14 @@ func Between(val int, min int, max int) bool { func IsTty() bool { return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 } + +func TrimRight(runes *[]rune) []rune { + var i int + for i = len(*runes) - 1; i >= 0; i-- { + char := (*runes)[i] + if char != ' ' && char != '\t' { + break + } + } + return (*runes)[0 : i+1] +} From f66d94c6b06c58141c40935bfb18a45858cf79da Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 18 Apr 2015 02:52:30 +0900 Subject: [PATCH 204/408] Add `--color=[dark|light|16|bw]` option - dark: the current default for 256-color terminal - light: color scheme for 256-color terminal with light background - 16: the default color scheme for 16-color terminal (`+2`) - bw: no colors (`+c`) --- CHANGELOG.md | 4 ++ fzf | 4 +- man/man1/fzf.1 | 20 ++++++--- src/core.go | 2 +- src/curses/curses.go | 98 ++++++++++++++++++++++++++++++-------------- src/options.go | 41 ++++++++++++++---- src/terminal.go | 2 +- 7 files changed, 124 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d87876d..8f15f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ CHANGELOG - Performance optimization - Less aggressive memoization to limit memory usage +### New features + +- Added color scheme for light background: `--color=light` + 0.9.9 ----- diff --git a/fzf b/fzf index 1bc5673..f1838b8 100755 --- a/fzf +++ b/fzf @@ -206,10 +206,10 @@ class FZF @expect = true when /^--expect=(.*)$/ @expect = true - when '--toggle-sort', '--tiebreak' + when '--toggle-sort', '--tiebreak', '--color' argv.shift when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll', - /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/ + /^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/ # XXX else usage 1, "illegal option: #{o}" diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index cff8160..4626eba 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -91,11 +91,21 @@ Enable processing of ANSI color codes .B "--no-mouse" Disable mouse .TP -.B "+c, --no-color" -Disable colors -.TP -.B "+2, --no-256" -Disable 256-color +.B "--color=COL" +Color scheme: [dark|light|16|bw] +.br +(default: dark on 256-color terminal, otherwise 16) +.br +.R "" +.br +.BR dark " Dark color scheme for 256-color terminal" +.br +.BR light " Light color scheme for 256-color terminal" +.br +.BR 16 " Default color scheme for 16-color terminal" +.br +.BR bw " No colors" +.br .TP .B "--black" Use black background diff --git a/src/core.go b/src/core.go index 7230cff..d7a6a0b 100644 --- a/src/core.go +++ b/src/core.go @@ -68,7 +68,7 @@ func Run(options *Options) { return data, nil } if opts.Ansi { - if opts.Color { + if opts.Theme != nil { ansiProcessor = func(data *string) (*string, []ansiOffset) { return extractColor(data) } diff --git a/src/curses/curses.go b/src/curses/curses.go index 3340a5c..f326361 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -95,6 +95,18 @@ const ( doubleClickDuration = 500 * time.Millisecond ) +type ColorTheme struct { + darkBg C.short + prompt C.short + match C.short + current C.short + currentMatch C.short + spinner C.short + info C.short + cursor C.short + selected C.short +} + type Event struct { Type int Char rune @@ -117,6 +129,9 @@ var ( _colorMap map[int]int _prevDownTime time.Time _clickY []int + Default16 *ColorTheme + Dark256 *ColorTheme + Light256 *ColorTheme DarkBG C.short ) @@ -124,6 +139,36 @@ func init() { _prevDownTime = time.Unix(0, 0) _clickY = []int{} _colorMap = make(map[int]int) + Default16 = &ColorTheme{ + darkBg: C.COLOR_BLACK, + prompt: C.COLOR_BLUE, + match: C.COLOR_GREEN, + current: C.COLOR_YELLOW, + currentMatch: C.COLOR_GREEN, + spinner: C.COLOR_GREEN, + info: C.COLOR_WHITE, + cursor: C.COLOR_RED, + selected: C.COLOR_MAGENTA} + Dark256 = &ColorTheme{ + darkBg: 236, + prompt: 110, + match: 108, + current: 254, + currentMatch: 151, + spinner: 148, + info: 144, + cursor: 161, + selected: 168} + Light256 = &ColorTheme{ + darkBg: 251, + prompt: 25, + match: 66, + current: 237, + currentMatch: 23, + spinner: 65, + info: 101, + cursor: 161, + selected: 168} } func attrColored(pair int, bold bool) C.int { @@ -173,7 +218,7 @@ func getch(nonblock bool) int { return int(b[0]) } -func Init(color bool, color256 bool, black bool, mouse bool) { +func Init(theme *ColorTheme, black bool, mouse bool) { { in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) if err != nil { @@ -203,42 +248,35 @@ func Init(color bool, color256 bool, black bool, mouse bool) { os.Exit(1) }() - if color { + if theme != nil { C.start_color() - var bg C.short - if black { - bg = C.COLOR_BLACK - } else { - C.use_default_colors() - bg = -1 - } - if color256 { - DarkBG = 236 - C.init_pair(ColPrompt, 110, bg) - C.init_pair(ColMatch, 108, bg) - C.init_pair(ColCurrent, 254, DarkBG) - C.init_pair(ColCurrentMatch, 151, DarkBG) - C.init_pair(ColSpinner, 148, bg) - C.init_pair(ColInfo, 144, bg) - C.init_pair(ColCursor, 161, DarkBG) - C.init_pair(ColSelected, 168, DarkBG) - } else { - DarkBG = C.COLOR_BLACK - C.init_pair(ColPrompt, C.COLOR_BLUE, bg) - C.init_pair(ColMatch, C.COLOR_GREEN, bg) - C.init_pair(ColCurrent, C.COLOR_YELLOW, DarkBG) - C.init_pair(ColCurrentMatch, C.COLOR_GREEN, DarkBG) - C.init_pair(ColSpinner, C.COLOR_GREEN, bg) - C.init_pair(ColInfo, C.COLOR_WHITE, bg) - C.init_pair(ColCursor, C.COLOR_RED, DarkBG) - C.init_pair(ColSelected, C.COLOR_MAGENTA, DarkBG) - } + initPairs(theme, black) _color = attrColored } else { _color = attrMono } } +func initPairs(theme *ColorTheme, black bool) { + var bg C.short + if black { + bg = C.COLOR_BLACK + } else { + C.use_default_colors() + bg = -1 + } + + DarkBG = theme.darkBg + C.init_pair(ColPrompt, theme.prompt, bg) + C.init_pair(ColMatch, theme.match, bg) + C.init_pair(ColCurrent, theme.current, DarkBG) + C.init_pair(ColCurrentMatch, theme.currentMatch, DarkBG) + C.init_pair(ColSpinner, theme.spinner, bg) + C.init_pair(ColInfo, theme.info, bg) + C.init_pair(ColCursor, theme.cursor, DarkBG) + C.init_pair(ColSelected, theme.selected, DarkBG) +} + func Close() { C.endwin() C.swapOutput() diff --git a/src/options.go b/src/options.go index d13a53b..173dee9 100644 --- a/src/options.go +++ b/src/options.go @@ -35,8 +35,8 @@ const usage = `usage: fzf [options] -m, --multi Enable multi-select with tab/shift-tab --ansi Enable processing of ANSI color codes --no-mouse Disable mouse - +c, --no-color Disable colors - +2, --no-256 Disable 256-color + --color=COL Color scheme [dark|light|16|bw] + (default: dark on 256-color terminal, otherwise 16) --black Use black background --reverse Reverse orientation --no-hscroll Disable horizontal scroll @@ -101,8 +101,7 @@ type Options struct { Multi bool Ansi bool Mouse bool - Color bool - Color256 bool + Theme *curses.ColorTheme Black bool Reverse bool Hscroll bool @@ -119,6 +118,13 @@ type Options struct { } func defaultOptions() *Options { + var defaultTheme *curses.ColorTheme + if strings.Contains(os.Getenv("TERM"), "256") { + defaultTheme = curses.Dark256 + } else { + defaultTheme = curses.Default16 + } + return &Options{ Mode: ModeFuzzy, Case: CaseSmart, @@ -131,8 +137,7 @@ func defaultOptions() *Options { Multi: false, Ansi: false, Mouse: true, - Color: true, - Color256: strings.Contains(os.Getenv("TERM"), "256"), + Theme: defaultTheme, Black: false, Reverse: false, Hscroll: true, @@ -266,6 +271,22 @@ func parseTiebreak(str string) tiebreak { return byLength } +func parseTheme(str string) *curses.ColorTheme { + switch strings.ToLower(str) { + case "dark": + return curses.Dark256 + case "light": + return curses.Light256 + case "16": + return curses.Default16 + case "bw", "off", "no", "none": + return nil + default: + errorExit("invalid color scheme: " + str) + } + return nil +} + func checkToggleSort(str string) int { keys := parseKeyChords(str, "key name required") if len(keys) != 1 { @@ -295,6 +316,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") case "--tiebreak": opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) + case "--color": + opts.Theme = parseTheme(nextString(allArgs, &i, "color scheme name required")) case "--toggle-sort": opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required")) case "-d", "--delimiter": @@ -326,9 +349,9 @@ func parseOptions(opts *Options, allArgs []string) { case "--no-mouse": opts.Mouse = false case "+c", "--no-color": - opts.Color = false + opts.Theme = nil case "+2", "--no-256": - opts.Color256 = false + opts.Theme = curses.Default16 case "--black": opts.Black = true case "--no-black": @@ -384,6 +407,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Expect = parseKeyChords(value, "key names required") } else if match, value := optString(arg, "--tiebreak="); match { opts.Tiebreak = parseTiebreak(value) + } else if match, value := optString(arg, "--color="); match { + opts.Theme = parseTheme(value) } else { errorExit("unknown option: " + arg) } diff --git a/src/terminal.go b/src/terminal.go index eaec9a3..959c84d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -105,7 +105,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { suppress: true, startChan: make(chan bool, 1), initFunc: func() { - C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse) + C.Init(opts.Theme, opts.Black, opts.Mouse) }} } From 1169cc86538e6a7659b64ac5a52c1fb7d37b759d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 18 Apr 2015 10:38:12 +0900 Subject: [PATCH 205/408] 0.9.10 --- install | 2 +- man/man1/fzf.1 | 8 ++++---- src/constants.go | 2 +- src/options.go | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/install b/install index 038e1f3..2b32cbf 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.9 +version=0.9.10 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 4626eba..ec0def7 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "April 2015" "fzf 0.9.9" "fzf - a command-line fuzzy finder" +.TH fzf 1 "April 2015" "fzf 0.9.10" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -98,11 +98,11 @@ Color scheme: [dark|light|16|bw] .br .R "" .br -.BR dark " Dark color scheme for 256-color terminal" +.BR dark " Color scheme for dark 256-color terminal" .br -.BR light " Light color scheme for 256-color terminal" +.BR light " Color scheme for light 256-color terminal" .br -.BR 16 " Default color scheme for 16-color terminal" +.BR 16 " Color scheme for 16-color terminal" .br .BR bw " No colors" .br diff --git a/src/constants.go b/src/constants.go index 07ac752..b204cf9 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - Version = "0.9.9" + Version = "0.9.10" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond diff --git a/src/options.go b/src/options.go index 173dee9..25dcfb3 100644 --- a/src/options.go +++ b/src/options.go @@ -35,7 +35,7 @@ const usage = `usage: fzf [options] -m, --multi Enable multi-select with tab/shift-tab --ansi Enable processing of ANSI color codes --no-mouse Disable mouse - --color=COL Color scheme [dark|light|16|bw] + --color=COL Color scheme; [dark|light|16|bw] (default: dark on 256-color terminal, otherwise 16) --black Use black background --reverse Reverse orientation @@ -279,7 +279,7 @@ func parseTheme(str string) *curses.ColorTheme { return curses.Light256 case "16": return curses.Default16 - case "bw", "off", "no", "none": + case "bw", "no": return nil default: errorExit("invalid color scheme: " + str) From d2f95d69fbf40d77b9b61b7e24989d607e3790a9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 18 Apr 2015 16:24:57 +0900 Subject: [PATCH 206/408] [fzf-tmux] Fix #200 - Double-quote handling Related #199 --- bin/fzf-tmux | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 55ea5f7..fb00edb 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -89,19 +89,27 @@ fi set -e -# Build arguments to fzf -[ ${#args[@]} -gt 0 ] && fzf_args=$(printf '\\"%s\\" ' "${args[@]}"; echo '') - # Clean up named pipes on exit id=$RANDOM +argsf=/tmp/fzf-args-$id fifo1=/tmp/fzf-fifo1-$id fifo2=/tmp/fzf-fifo2-$id fifo3=/tmp/fzf-fifo3-$id cleanup() { - rm -f $fifo1 $fifo2 $fifo3 + rm -f $argsf $fifo1 $fifo2 $fifo3 } trap cleanup EXIT SIGINT SIGTERM +# Build arguments to fzf +touch $argsf +if [ ${#args[@]} -gt 0 ]; then + str="" + for arg in "${args[@]}"; do + str="$str \"${arg//\"/\\\"}\"" + done + cat <<< "$str" > $argsf +fi + fail() { >&2 echo "$1" exit 1 @@ -117,11 +125,11 @@ mkfifo $fifo2 mkfifo $fifo3 if [ -n "$term" -o -t 0 ]; then tmux set-window-option -q synchronize-panes off \;\ - split-window $opt "cd $(printf %q "$PWD");$envs"' sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "cd $(printf %q "$PWD");$envs"' sh -c "'$fzf' $(cat '$argsf') > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap else mkfifo $fifo1 tmux set-window-option -q synchronize-panes off \;\ - split-window $opt "$envs"' sh -c "'$fzf' '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "$envs"' sh -c "'$fzf' $(cat '$argsf') < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap cat <&0 > $fifo1 & fi cat $fifo2 From d091a2c4bb47b0b9abe52d5941bfbfeb6a8dc644 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 18 Apr 2015 16:27:40 +0900 Subject: [PATCH 207/408] [fzf-tmux] Minor adjustment --- bin/fzf-tmux | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index fb00edb..8c16eb0 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -101,14 +101,11 @@ cleanup() { trap cleanup EXIT SIGINT SIGTERM # Build arguments to fzf -touch $argsf -if [ ${#args[@]} -gt 0 ]; then - str="" - for arg in "${args[@]}"; do - str="$str \"${arg//\"/\\\"}\"" - done - cat <<< "$str" > $argsf -fi +str="" +for arg in "${args[@]}"; do + str="$str \"${arg//\"/\\\"}\"" +done +cat <<< "$str" > $argsf fail() { >&2 echo "$1" From 537d07c1e5133727dddbd6a345f5926719a3d7b4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 19 Apr 2015 17:13:07 +0900 Subject: [PATCH 208/408] [vim] Use "system" fzf when available 1. Go binary: ../bin/fzf 2. System fzf: $(which fzf) 3. Download fzf from GitHub or create wrapper script to Ruby version (../fzf) when the binary for the platform is not available 4. If install script is not found or for some reason failed, try to use Ruby version in its expected location (../fzf) 5. If fzf is found to be a shell function, use it (type fzf) --- plugin/fzf.vim | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 7015e28..8746af0 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -36,17 +36,17 @@ function! s:fzf_exec() if !exists('s:exec') if executable(s:fzf_go) let s:exec = s:fzf_go - elseif !s:installed && executable(s:install) - echohl WarningMsg - echo 'Downloading fzf binary. Please wait ...' - echohl None - let s:installed = 1 - call system(s:install.' --bin') - return s:fzf_exec() else let path = split(system('which fzf 2> /dev/null'), '\n') if !v:shell_error && !empty(path) let s:exec = path[0] + elseif !s:installed && executable(s:install) + echohl WarningMsg + echo 'Downloading fzf binary. Please wait ...' + echohl None + let s:installed = 1 + call system(s:install.' --bin') + return s:fzf_exec() elseif executable(s:fzf_rb) let s:exec = s:fzf_rb else From 869a234938421da7b5318a81d22cd6b3efe1efd1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 20 Apr 2015 14:58:27 +0900 Subject: [PATCH 209/408] [fzf-tmux] Use bash instead of sh (#204) The default shell can be a non-standard shell (e.g. fish) --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 8c16eb0..c219480 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -122,11 +122,11 @@ mkfifo $fifo2 mkfifo $fifo3 if [ -n "$term" -o -t 0 ]; then tmux set-window-option -q synchronize-panes off \;\ - split-window $opt "cd $(printf %q "$PWD");$envs"' sh -c "'$fzf' $(cat '$argsf') > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "cd $(printf %q "$PWD");$envs"' bash -c "'$fzf' $(cat '$argsf') > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap else mkfifo $fifo1 tmux set-window-option -q synchronize-panes off \;\ - split-window $opt "$envs"' sh -c "'$fzf' $(cat '$argsf') < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "$envs"' bash -c "'$fzf' $(cat '$argsf') < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap cat <&0 > $fifo1 & fi cat $fifo2 From 7b6a27cb5e15c7bb749ffc9adbc74f852c3ad6c2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 20 Apr 2015 15:22:59 +0900 Subject: [PATCH 210/408] [fzf-tmux] Fix #204 - Escape command substitution --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index c219480..6b81446 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -122,11 +122,11 @@ mkfifo $fifo2 mkfifo $fifo3 if [ -n "$term" -o -t 0 ]; then tmux set-window-option -q synchronize-panes off \;\ - split-window $opt "cd $(printf %q "$PWD");$envs"' bash -c "'$fzf' $(cat '$argsf') > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "cd $(printf %q "$PWD");$envs"' bash -c "'$fzf' \$(cat '$argsf') > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap else mkfifo $fifo1 tmux set-window-option -q synchronize-panes off \;\ - split-window $opt "$envs"' bash -c "'$fzf' $(cat '$argsf') < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "$envs"' bash -c "'$fzf' \$(cat '$argsf') < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap cat <&0 > $fifo1 & fi cat $fifo2 From 16ca9c688b6f6c5607ebf78f67ca66e7f250db7a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 20 Apr 2015 16:23:15 +0900 Subject: [PATCH 211/408] Revert "[fzf-tmux] Fix #204 - Escape command substitution" This reverts commit 7b6a27cb5e15c7bb749ffc9adbc74f852c3ad6c2. --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 6b81446..c219480 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -122,11 +122,11 @@ mkfifo $fifo2 mkfifo $fifo3 if [ -n "$term" -o -t 0 ]; then tmux set-window-option -q synchronize-panes off \;\ - split-window $opt "cd $(printf %q "$PWD");$envs"' bash -c "'$fzf' \$(cat '$argsf') > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "cd $(printf %q "$PWD");$envs"' bash -c "'$fzf' $(cat '$argsf') > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap else mkfifo $fifo1 tmux set-window-option -q synchronize-panes off \;\ - split-window $opt "$envs"' bash -c "'$fzf' \$(cat '$argsf') < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "$envs"' bash -c "'$fzf' $(cat '$argsf') < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap cat <&0 > $fifo1 & fi cat $fifo2 From 917b1759b08e1105ce964eff23aae8a304a12eee Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 20 Apr 2015 22:42:02 +0900 Subject: [PATCH 212/408] [fzf-tmux/vim] Fixes for fish (#204) --- bin/fzf-tmux | 20 +++++++++++--------- plugin/fzf.vim | 6 ++++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index c219480..fe9f8ad 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -100,13 +100,6 @@ cleanup() { } trap cleanup EXIT SIGINT SIGTERM -# Build arguments to fzf -str="" -for arg in "${args[@]}"; do - str="$str \"${arg//\"/\\\"}\"" -done -cat <<< "$str" > $argsf - fail() { >&2 echo "$1" exit 1 @@ -120,13 +113,22 @@ envs="" mkfifo $fifo2 mkfifo $fifo3 + +# Build arguments to fzf +opts="" +for arg in "${args[@]}"; do + opts="$opts \"${arg//\"/\\\"}\"" +done + if [ -n "$term" -o -t 0 ]; then + cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option -q synchronize-panes off \;\ - split-window $opt "cd $(printf %q "$PWD");$envs"' bash -c "'$fzf' $(cat '$argsf') > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap else mkfifo $fifo1 + cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option -q synchronize-panes off \;\ - split-window $opt "$envs"' bash -c "'$fzf' $(cat '$argsf') < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap + split-window $opt "$envs bash $argsf" $swap cat <&0 > $fifo1 & fi cat $fifo2 diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 8746af0..28efe69 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -105,6 +105,9 @@ function! s:upgrade(dict) endfunction function! fzf#run(...) abort +try + let oshell = &shell + set shell=sh if has('nvim') && bufexists('[FZF]') echohl WarningMsg echomsg 'FZF is already running!' @@ -149,6 +152,9 @@ function! fzf#run(...) abort finally call s:popd(dict) endtry +finally + let &shell = oshell +endtry endfunction function! s:present(dict, ...) From 3f0e6a5806aba76037d53476e991cce045de16e4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 21 Apr 2015 22:10:14 +0900 Subject: [PATCH 213/408] Fix #209 - Invalid mutation of input on case conversion --- src/algo/algo.go | 9 +++++++-- test/test_go.rb | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index c1c07f3..3d5edb0 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -42,10 +42,8 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { // compiler as of now does not inline non-leaf functions.) if char >= 'A' && char <= 'Z' { char += 32 - (*runes)[index] = char } else if char > unicode.MaxASCII { char = unicode.To(unicode.LowerCase, char) - (*runes)[index] = char } } if char == pattern[pidx] { @@ -63,6 +61,13 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { pidx-- for index := eidx - 1; index >= sidx; index-- { char := (*runes)[index] + if !caseSensitive { + if char >= 'A' && char <= 'Z' { + char += 32 + } else if char > unicode.MaxASCII { + char = unicode.To(unicode.LowerCase, char) + } + } if char == pattern[pidx] { if pidx--; pidx < 0 { sidx = index diff --git a/test/test_go.rb b/test/test_go.rb index 3a9f1fd..4e255be 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -525,6 +525,16 @@ class TestGoFZF < TestBase File.unlink tempname end + def test_invalid_cache + tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter + tmux.until { |lines| lines[-2].include? '2/3' } + tmux.send_keys :BSpace + tmux.until { |lines| lines[-2].include? '3/3' } + tmux.send_keys :D + tmux.until { |lines| lines[-2].include? '1/3' } + tmux.send_keys :Enter + end + private def writelines path, lines, timeout = 10 File.open(path, 'w') do |f| From e82eb277878d96d977a24688a1ed3941861f0788 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 21 Apr 2015 22:18:05 +0900 Subject: [PATCH 214/408] Smart-case for each term in extended-search mode Close #208 --- src/pattern.go | 62 ++++++++++++++++++++++++------------------------- test/test_go.rb | 4 ++++ 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 75cc5f8..64e0c6e 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -4,7 +4,6 @@ import ( "regexp" "sort" "strings" - "unicode" "github.com/junegunn/fzf/src/algo" ) @@ -28,10 +27,11 @@ const ( ) type term struct { - typ termType - inv bool - text []rune - origText []rune + typ termType + inv bool + text []rune + caseSensitive bool + origText []rune } // Pattern represents search pattern @@ -88,36 +88,27 @@ func BuildPattern(mode Mode, caseMode Case, caseSensitive, hasInvTerm := true, false terms := []term{} - switch caseMode { - case CaseSmart: - hasUppercase := false - for _, r := range runes { - if unicode.IsUpper(r) { - hasUppercase = true - break - } - } - if !hasUppercase { - runes, caseSensitive = []rune(strings.ToLower(asString)), false - } - case CaseIgnore: - runes, caseSensitive = []rune(strings.ToLower(asString)), false - } - switch mode { case ModeExtended, ModeExtendedExact: - terms = parseTerms(mode, string(runes)) + terms = parseTerms(mode, caseMode, asString) for _, term := range terms { if term.inv { hasInvTerm = true } } + default: + lowerString := strings.ToLower(asString) + caseSensitive = caseMode == CaseRespect || + caseMode == CaseSmart && lowerString != asString + if !caseSensitive { + asString = lowerString + } } ptr := &Pattern{ mode: mode, caseSensitive: caseSensitive, - text: runes, + text: []rune(asString), terms: terms, hasInvTerm: hasInvTerm, nth: nth, @@ -133,11 +124,17 @@ func BuildPattern(mode Mode, caseMode Case, return ptr } -func parseTerms(mode Mode, str string) []term { +func parseTerms(mode Mode, caseMode Case, str string) []term { tokens := _splitRegex.Split(str, -1) terms := []term{} for _, token := range tokens { typ, inv, text := termFuzzy, false, token + lowerText := strings.ToLower(text) + caseSensitive := caseMode == CaseRespect || + caseMode == CaseSmart && text != lowerText + if !caseSensitive { + text = lowerText + } origText := []rune(text) if mode == ModeExtendedExact { typ = termExact @@ -163,10 +160,11 @@ func parseTerms(mode Mode, str string) []term { if len(text) > 0 { terms = append(terms, term{ - typ: typ, - inv: inv, - text: []rune(text), - origText: origText}) + typ: typ, + inv: inv, + text: []rune(text), + caseSensitive: caseSensitive, + origText: origText}) } } return terms @@ -280,7 +278,7 @@ func dupItem(item *Item, offsets []Offset) *Item { func (p *Pattern) fuzzyMatch(item *Item) (int, int) { input := p.prepareInput(item) - return p.iter(algo.FuzzyMatch, input, p.text) + return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.text) } func (p *Pattern) extendedMatch(item *Item) []Offset { @@ -288,7 +286,7 @@ func (p *Pattern) extendedMatch(item *Item) []Offset { offsets := []Offset{} for _, term := range p.terms { pfun := p.procFun[term.typ] - if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { + if sidx, eidx := p.iter(pfun, input, term.caseSensitive, term.text); sidx >= 0 { if term.inv { break } @@ -319,10 +317,10 @@ func (p *Pattern) prepareInput(item *Item) *[]Token { } func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int), - tokens *[]Token, pattern []rune) (int, int) { + tokens *[]Token, caseSensitive bool, pattern []rune) (int, int) { for _, part := range *tokens { prefixLength := part.prefixLength - if sidx, eidx := pfun(p.caseSensitive, part.text, pattern); sidx >= 0 { + if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 { return sidx + prefixLength, eidx + prefixLength } } diff --git a/test/test_go.rb b/test/test_go.rb index 4e255be..92d1544 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -535,6 +535,10 @@ class TestGoFZF < TestBase tmux.send_keys :Enter end + def test_smart_case_for_each_term + assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i + end + private def writelines path, lines, timeout = 10 File.open(path, 'w') do |f| From a88bf87e2ae2fada2bd3d179ba67d681a7913f35 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 21 Apr 2015 22:36:40 +0900 Subject: [PATCH 215/408] Update test case --- src/pattern_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pattern_test.go b/src/pattern_test.go index c73e650..7f00272 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -7,7 +7,7 @@ import ( ) func TestParseTermsExtended(t *testing.T) { - terms := parseTerms(ModeExtended, + terms := parseTerms(ModeExtended, CaseSmart, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") if len(terms) != 8 || terms[0].typ != termFuzzy || terms[0].inv || @@ -31,7 +31,7 @@ func TestParseTermsExtended(t *testing.T) { } func TestParseTermsExtendedExact(t *testing.T) { - terms := parseTerms(ModeExtendedExact, + terms := parseTerms(ModeExtendedExact, CaseSmart, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") if len(terms) != 8 || terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 || @@ -47,7 +47,7 @@ func TestParseTermsExtendedExact(t *testing.T) { } func TestParseTermsEmpty(t *testing.T) { - terms := parseTerms(ModeExtended, "' $ ^ !' !^ !$") + terms := parseTerms(ModeExtended, CaseSmart, "' $ ^ !' !^ !$") if len(terms) != 0 { t.Errorf("%s", terms) } From 9f64a00549afc77466eac5abfaeec98effec1ddc Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 21 Apr 2015 23:23:39 +0900 Subject: [PATCH 216/408] Fix double-click result when scroll offset is positive --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 959c84d..a8a85da 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -697,7 +697,7 @@ func (t *Terminal) Loop() { } else if me.Double { // Double-click if my >= 2 { - if t.vset(my-2) && t.cy < t.merger.Length() { + if t.vset(t.offset+my-2) && t.cy < t.merger.Length() { req(reqClose) } } From 651a8f8cc20853341e4649e8246d3195c713961d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 21 Apr 2015 23:50:53 +0900 Subject: [PATCH 217/408] Add --inline-info option Close #202 --- man/man1/fzf.1 | 3 +++ src/options.go | 7 ++++++ src/terminal.go | 62 +++++++++++++++++++++++++++++++++++-------------- 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index ec0def7..04e7fc6 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -116,6 +116,9 @@ Reverse orientation .B "--no-hscroll" Disable horizontal scroll .TP +.B "--inline-info" +Display finder info inline with the query +.TP .BI "--prompt=" "STR" Input prompt (default: '> ') .SS Scripting diff --git a/src/options.go b/src/options.go index 25dcfb3..723c261 100644 --- a/src/options.go +++ b/src/options.go @@ -40,6 +40,7 @@ const usage = `usage: fzf [options] --black Use black background --reverse Reverse orientation --no-hscroll Disable horizontal scroll + --inline-info Display finder info inline with the query --prompt=STR Input prompt (default: '> ') Scripting @@ -105,6 +106,7 @@ type Options struct { Black bool Reverse bool Hscroll bool + InlineInfo bool Prompt string Query string Select1 bool @@ -141,6 +143,7 @@ func defaultOptions() *Options { Black: false, Reverse: false, Hscroll: true, + InlineInfo: false, Prompt: "> ", Query: "", Select1: false, @@ -364,6 +367,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Hscroll = true case "--no-hscroll": opts.Hscroll = false + case "--inline-info": + opts.InlineInfo = true + case "--no-inline-info": + opts.InlineInfo = false case "-1", "--select-1": opts.Select1 = true case "+1", "--no-select-1": diff --git a/src/terminal.go b/src/terminal.go index a8a85da..cae8bf7 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -20,6 +20,7 @@ import ( // Terminal represents terminal input/output type Terminal struct { + inlineInfo bool prompt string reverse bool hscroll bool @@ -83,6 +84,7 @@ const ( func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) return &Terminal{ + inlineInfo: opts.InlineInfo, prompt: opts.Prompt, reverse: opts.Reverse, hscroll: opts.Hscroll, @@ -229,14 +231,23 @@ func (t *Terminal) printPrompt() { } func (t *Terminal) printInfo() { - t.move(1, 0, true) - if t.reading { - duration := int64(spinnerDuration) - idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration - C.CPrint(C.ColSpinner, true, _spinner[idx]) + if t.inlineInfo { + t.move(0, len(t.prompt)+displayWidth(t.input)+1, true) + if t.reading { + C.CPrint(C.ColSpinner, true, " < ") + } else { + C.CPrint(C.ColPrompt, true, " < ") + } + } else { + t.move(1, 0, true) + if t.reading { + duration := int64(spinnerDuration) + idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration + C.CPrint(C.ColSpinner, true, _spinner[idx]) + } + t.move(1, 2, false) } - t.move(1, 2, false) output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) if t.toggleSort > 0 { if t.sort { @@ -257,10 +268,16 @@ func (t *Terminal) printInfo() { func (t *Terminal) printList() { t.constrain() - maxy := maxItems() + maxy := t.maxItems() count := t.merger.Length() - t.offset for i := 0; i < maxy; i++ { - t.move(i+2, 0, true) + var line int + if t.inlineInfo { + line = i + 1 + } else { + line = i + 2 + } + t.move(line, 0, true) if i < count { t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset) } @@ -515,6 +532,9 @@ func (t *Terminal) Loop() { switch req { case reqPrompt: t.printPrompt() + if t.inlineInfo { + t.printInfo() + } case reqInfo: t.printInfo() case reqList: @@ -659,10 +679,10 @@ func (t *Terminal) Loop() { case C.Del: t.delChar() case C.PgUp: - t.vmove(maxItems() - 1) + t.vmove(t.maxItems() - 1) req(reqList) case C.PgDn: - t.vmove(-(maxItems() - 1)) + t.vmove(-(t.maxItems() - 1)) req(reqList) case C.AltB: t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 @@ -685,6 +705,10 @@ func (t *Terminal) Loop() { if !t.reverse { my = C.MaxY() - my - 1 } + min := 2 + if t.inlineInfo { + min = 1 + } if me.S != 0 { // Scroll if t.merger.Length() > 0 { @@ -696,8 +720,8 @@ func (t *Terminal) Loop() { } } else if me.Double { // Double-click - if my >= 2 { - if t.vset(t.offset+my-2) && t.cy < t.merger.Length() { + if my >= min { + if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { req(reqClose) } } @@ -705,9 +729,9 @@ func (t *Terminal) Loop() { if my == 0 && mx >= 0 { // Prompt t.cx = mx - } else if my >= 2 { + } else if my >= min { // List - if t.vset(t.offset+my-2) && t.multi && me.Mod { + if t.vset(t.offset+my-min) && t.multi && me.Mod { toggle() } req(reqList) @@ -728,7 +752,7 @@ func (t *Terminal) Loop() { func (t *Terminal) constrain() { count := t.merger.Length() - height := C.MaxY() - 2 + height := t.maxItems() diffpos := t.cy - t.offset t.cy = util.Constrain(t.cy, 0, count-1) @@ -761,6 +785,10 @@ func (t *Terminal) vset(o int) bool { return t.cy == o } -func maxItems() int { - return C.MaxY() - 2 +func (t *Terminal) maxItems() int { + if t.inlineInfo { + return C.MaxY() - 1 + } else { + return C.MaxY() - 2 + } } From 318edc8c352250be04a9c2d15f5854084671c3df Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Apr 2015 00:30:09 +0900 Subject: [PATCH 218/408] Apply fzf-tmux to key bindings (#203) Note that CTRL-T on bash is still using the old trick of send-keys. --- shell/key-bindings.bash | 30 +++++++++++++++++------------- shell/key-bindings.fish | 31 +++++++------------------------ shell/key-bindings.zsh | 27 +++++++-------------------- 3 files changed, 31 insertions(+), 57 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index d983bb1..dfa9bf6 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -1,6 +1,6 @@ # Key bindings # ------------ -__fsel() { +__fzf_select__() { command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ -o -type f -print \ -o -type d -print \ @@ -12,7 +12,7 @@ __fsel() { if [[ $- =~ i ]]; then -__fsel_tmux() { +__fzf_select_tmux__() { local height height=${FZF_TMUX_HEIGHT:-40%} if [[ $height =~ %$ ]]; then @@ -20,13 +20,17 @@ __fsel_tmux() { else height="-l $height" fi - tmux split-window $height "cd $(printf %q "$PWD");bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'" + tmux split-window $height "cd $(printf %q "$PWD");bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" } -__fcd() { +__fzf_cd__() { local dir dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ - -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m) && printf 'cd %q' "$dir" + -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf-tmux -d${FZF_TMUX_HEIGHT:-40%} +m) && printf 'cd %q' "$dir" +} + +__fzf_history__() { + HISTTIMEFORMAT= history | fzf-tmux -d${FZF_TMUX_HEIGHT:-40%} +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed "s/ *[0-9]* *//" } __use_tmux=0 @@ -38,16 +42,16 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then # CTRL-T - Paste the selected file path into the command line if [ $__use_tmux -eq 1 ]; then - bind '"\C-t": " \C-u \C-a\C-k$(__fsel_tmux)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' + bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select_tmux__)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' else - bind '"\C-t": " \C-u \C-a\C-k$(__fsel)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' + bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select__)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' fi # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed \"s/ *[0-9]* *//\")\e\C-e\er"' + bind '"\C-r": " \C-e\C-u$(__fzf_history__)\e\C-e\er"' # ALT-C - cd into the selected directory - bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' + bind '"\ec": " \C-e\C-u$(__fzf_cd__)\e\C-e\er\C-m"' else bind '"\C-x\C-e": shell-expand-line' bind '"\C-x\C-r": redraw-current-line' @@ -55,18 +59,18 @@ else # CTRL-T - Paste the selected file path into the command line # - FIXME: Selected items are attached to the end regardless of cursor position if [ $__use_tmux -eq 1 ]; then - bind '"\C-t": "\e$a \eddi$(__fsel_tmux)\C-x\C-e\e0P$xa"' + bind '"\C-t": "\e$a \eddi$(__fzf_select_tmux__)\C-x\C-e\e0P$xa"' else - bind '"\C-t": "\e$a \eddi$(__fsel)\C-x\C-e\e0Px$a \C-x\C-r\exa "' + bind '"\C-t": "\e$a \eddi$(__fzf_select__)\C-x\C-e\e0Px$a \C-x\C-r\exa "' fi bind -m vi-command '"\C-t": "i\C-t"' # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' + bind '"\C-r": "\eddi$(__fzf_history__)\C-x\C-e\e$a\C-x\C-r"' bind -m vi-command '"\C-r": "i\C-r"' # ALT-C - cd into the selected directory - bind '"\ec": "\eddi$(__fcd)\C-x\C-e\C-x\C-r\C-m"' + bind '"\ec": "\eddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' bind -m vi-command '"\ec": "i\ec"' fi diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index f5f79c4..2caaf20 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -26,25 +26,14 @@ function fzf_key_bindings end function __fzf_ctrl_t - if [ -n "$TMUX_PANE" -a "$FZF_TMUX" != "0" ] - # FIXME need to handle directory with double-quotes - tmux split-window (__fzf_tmux_height) "cd \"$PWD\";fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'" - else - __fzf_list | fzf -m > $TMPDIR/fzf.result - and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) - commandline -f repaint - rm -f $TMPDIR/fzf.result - end - end - - function __fzf_ctrl_t_tmux - __fzf_list | fzf -m > $TMPDIR/fzf.result - and tmux send-keys -t $argv[1] (cat $TMPDIR/fzf.result | __fzf_escape) + __fzf_list | fzf-tmux (__fzf_tmux_height) -m > $TMPDIR/fzf.result + and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) + commandline -f repaint rm -f $TMPDIR/fzf.result end function __fzf_ctrl_r - history | fzf +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result + history | fzf-tmux (__fzf_tmux_height) +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result and commandline (cat $TMPDIR/fzf.result) commandline -f repaint rm -f $TMPDIR/fzf.result @@ -52,7 +41,7 @@ function fzf_key_bindings function __fzf_alt_c # Fish hangs if the command before pipe redirects (2> /dev/null) - __fzf_list_dir | fzf +m > $TMPDIR/fzf.result + __fzf_list_dir | fzf-tmux (__fzf_tmux_height) +m > $TMPDIR/fzf.result [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] and cd (cat $TMPDIR/fzf.result) commandline -f repaint @@ -61,16 +50,10 @@ function fzf_key_bindings function __fzf_tmux_height if set -q FZF_TMUX_HEIGHT - set height $FZF_TMUX_HEIGHT + echo "-d$FZF_TMUX_HEIGHT" else - set height 40% + echo "-d40%" end - if echo $height | \grep -q -E '%$' - echo "-p "(echo $height | sed 's/%$//') - else - echo "-l $height" - end - set -e height end bind \ct '__fzf_ctrl_t' diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 2bf42fa..40acafc 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -5,7 +5,7 @@ __fsel() { command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do + -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf-tmux -d${FZF_TMUX_HEIGHT:-40%} -m | while read item; do printf '%q ' "$item" done echo @@ -13,30 +13,17 @@ __fsel() { if [[ $- =~ i ]]; then -if [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then - fzf-file-widget() { - local height - height=${FZF_TMUX_HEIGHT:-40%} - if [[ $height =~ %$ ]]; then - height="-p ${height%\%}" - else - height="-l $height" - fi - tmux split-window $height "cd $(printf %q "$PWD");zsh -c 'source ~/.fzf.zsh; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'" - } -else - fzf-file-widget() { - LBUFFER="${LBUFFER}$(__fsel)" - zle redisplay - } -fi +fzf-file-widget() { + LBUFFER="${LBUFFER}$(__fsel)" + zle redisplay +} zle -N fzf-file-widget bindkey '^T' fzf-file-widget # ALT-C - cd into the selected directory fzf-cd-widget() { cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ - -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m):-.}" + -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf-tmux -d${FZF_TMUX_HEIGHT:-40%} +m):-.}" zle reset-prompt } zle -N fzf-cd-widget @@ -45,7 +32,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected - if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then + if selected=$(fc -l 1 | fzf-tmux -d${FZF_TMUX_HEIGHT:-40%} +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') LBUFFER=!$num zle expand-history From 06b4f7568084358fce6927c3bedc6816159607f0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Apr 2015 00:55:39 +0900 Subject: [PATCH 219/408] Fix broken FZF_TMUX switch and update test cases (#203) --- shell/key-bindings.bash | 8 ++++++-- shell/key-bindings.fish | 20 +++++++++++++------- shell/key-bindings.zsh | 10 +++++++--- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index dfa9bf6..362e442 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -12,6 +12,10 @@ __fzf_select__() { if [[ $- =~ i ]]; then +__fzfcmd() { + [ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" +} + __fzf_select_tmux__() { local height height=${FZF_TMUX_HEIGHT:-40%} @@ -26,11 +30,11 @@ __fzf_select_tmux__() { __fzf_cd__() { local dir dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ - -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf-tmux -d${FZF_TMUX_HEIGHT:-40%} +m) && printf 'cd %q' "$dir" + -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m) && printf 'cd %q' "$dir" } __fzf_history__() { - HISTTIMEFORMAT= history | fzf-tmux -d${FZF_TMUX_HEIGHT:-40%} +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed "s/ *[0-9]* *//" + HISTTIMEFORMAT= history | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed "s/ *[0-9]* *//" } __use_tmux=0 diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 2caaf20..8be5ab5 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -26,14 +26,14 @@ function fzf_key_bindings end function __fzf_ctrl_t - __fzf_list | fzf-tmux (__fzf_tmux_height) -m > $TMPDIR/fzf.result + __fzf_list | eval (__fzfcmd) -m > $TMPDIR/fzf.result and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) commandline -f repaint rm -f $TMPDIR/fzf.result end function __fzf_ctrl_r - history | fzf-tmux (__fzf_tmux_height) +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result + history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result and commandline (cat $TMPDIR/fzf.result) commandline -f repaint rm -f $TMPDIR/fzf.result @@ -41,18 +41,24 @@ function fzf_key_bindings function __fzf_alt_c # Fish hangs if the command before pipe redirects (2> /dev/null) - __fzf_list_dir | fzf-tmux (__fzf_tmux_height) +m > $TMPDIR/fzf.result + __fzf_list_dir | eval (__fzfcmd) +m > $TMPDIR/fzf.result [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] and cd (cat $TMPDIR/fzf.result) commandline -f repaint rm -f $TMPDIR/fzf.result end - function __fzf_tmux_height - if set -q FZF_TMUX_HEIGHT - echo "-d$FZF_TMUX_HEIGHT" + function __fzfcmd + set -q FZF_TMUX; or set FZF_TMUX 1 + + if [ $FZF_TMUX -eq 1 ] + if set -q FZF_TMUX_HEIGHT + echo "fzf-tmux -d$FZF_TMUX_HEIGHT" + else + echo "fzf-tmux -d40%" + end else - echo "-d40%" + echo "fzf" end end diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 40acafc..f8cbb71 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -5,12 +5,16 @@ __fsel() { command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf-tmux -d${FZF_TMUX_HEIGHT:-40%} -m | while read item; do + -o -type l -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) -m | while read item; do printf '%q ' "$item" done echo } +__fzfcmd() { + [ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" +} + if [[ $- =~ i ]]; then fzf-file-widget() { @@ -23,7 +27,7 @@ bindkey '^T' fzf-file-widget # ALT-C - cd into the selected directory fzf-cd-widget() { cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ - -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf-tmux -d${FZF_TMUX_HEIGHT:-40%} +m):-.}" + -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m):-.}" zle reset-prompt } zle -N fzf-cd-widget @@ -32,7 +36,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected - if selected=$(fc -l 1 | fzf-tmux -d${FZF_TMUX_HEIGHT:-40%} +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then + if selected=$(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') LBUFFER=!$num zle expand-history From edb5ab56222add7552c9180a65b29afbe0af9714 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Apr 2015 00:57:25 +0900 Subject: [PATCH 220/408] Update test cases for #203 --- test/test_go.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 92d1544..69285c8 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -582,11 +582,11 @@ module TestShell def test_alt_c tmux.prepare - tmux.send_keys :Escape, :c - lines = tmux.until { |lines| lines[-1].start_with? '>' } + tmux.send_keys :Escape, :c, pane: 0 + lines = tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } expected = lines[-3][2..-1] p expected - tmux.send_keys :Enter + tmux.send_keys :Enter, pane: 1 tmux.prepare tmux.send_keys :pwd, :Enter tmux.until { |lines| p lines; lines[-1].end_with?(expected) } @@ -599,11 +599,11 @@ module TestShell tmux.send_keys 'echo 3d', :Enter; tmux.prepare tmux.send_keys 'echo 3rd', :Enter; tmux.prepare tmux.send_keys 'echo 4th', :Enter; tmux.prepare - tmux.send_keys 'C-r' - tmux.until { |lines| lines[-1].start_with? '>' } - tmux.send_keys '3d' - tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort - tmux.send_keys :Enter + tmux.send_keys 'C-r', pane: 0 + tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } + tmux.send_keys '3d', pane: 1 + tmux.until(pane: 1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort + tmux.send_keys :Enter, pane: 1 tmux.until { |lines| lines[-1] == 'echo 3rd' } tmux.send_keys :Enter tmux.until { |lines| lines[-1] == '3rd' } From a4cf5510e36e02994549d2289074fffcf68ecf94 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Apr 2015 01:42:38 +0900 Subject: [PATCH 221/408] 0.9.11 --- CHANGELOG.md | 15 +++++++++++++++ fzf | 1 + install | 2 +- src/constants.go | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f15f76..be5d22d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ CHANGELOG ========= +0.9.11 +------ + +### New features + +- Added `--inline-info` option for saving screen estate (#202) + - Useful inside Neovim + - e.g. `let $FZF_DEFAULT_OPTS = $FZF_DEFAULT_OPTS.' --inline-info'` + +### Bug fixes + +- Invalid mutation of input on case conversion (#209) +- Smart-case for each term in extended-search mode (#208) +- Fixed double-click result when scroll offset is positive + 0.9.10 ------ diff --git a/fzf b/fzf index f1838b8..9e85e04 100755 --- a/fzf +++ b/fzf @@ -209,6 +209,7 @@ class FZF when '--toggle-sort', '--tiebreak', '--color' argv.shift when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll', + '--inline-info', '--no-inline-info', /^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/ # XXX else diff --git a/install b/install index 2b32cbf..ea89ee6 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.10 +version=0.9.11 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/src/constants.go b/src/constants.go index b204cf9..e3b4c86 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - Version = "0.9.10" + Version = "0.9.11" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 5e120e7ab52ac1e40e421fd7a9e941e3b32ba043 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Apr 2015 01:44:56 +0900 Subject: [PATCH 222/408] Update man page --- man/man1/fzf.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 04e7fc6..091adec 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "April 2015" "fzf 0.9.10" "fzf - a command-line fuzzy finder" +.TH fzf 1 "April 2015" "fzf 0.9.11" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder From 3b84c80d5666189820826c8e5496a488243fc442 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Apr 2015 02:07:27 +0900 Subject: [PATCH 223/408] Update README --- README.md | 87 +++++++++---------------------------------------------- 1 file changed, 14 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 9645a8a..a3157b9 100644 --- a/README.md +++ b/README.md @@ -45,17 +45,6 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install ``` -#### Using curl - -In case you don't have git installed: - -```sh -mkdir -p ~/.fzf -curl -L https://github.com/junegunn/fzf/archive/master.tar.gz | - tar xz --strip-components 1 -C ~/.fzf -~/.fzf/install -``` - #### Using Homebrew On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. @@ -157,18 +146,14 @@ fish. - Press `CTRL-R` again to toggle sort - `ALT-C` - cd into the selected directory -If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You -may disable this tmux integration by setting `FZF_TMUX` to 0, or change the -height of the window with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`). +If you're on a tmux session, fzf will start in a split pane. You may disable +this tmux integration by setting `FZF_TMUX` to 0, or change the height of the +pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`). If you use vi mode on bash, you need to add `set -o vi` *before* `source ~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi mode. -If you want to customize the key bindings, consider editing the -installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and -`~/.config/fish/functions/fzf_key_bindings.fish`. - `fzf-tmux` script ----------------- @@ -286,10 +271,10 @@ Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key, in new tabs, in horizontal splits, or in vertical splits respectively. Note that the environment variables `FZF_DEFAULT_COMMAND` and -`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][vim-examples] for +`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for customization. -[vim-examples]: https://github.com/junegunn/fzf/wiki/Examples-(vim) +[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim) #### `fzf#run([options])` @@ -375,6 +360,9 @@ page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). #### Articles - [fzf+vim+tmux](http://junegunn.kr/2014/04/fzf+vim+tmux) +- [Browsing git commits with fzf](http://junegunn.kr/2015/03/browsing-git-commits-with-fzf/) +- [Browsing Chrome history with fzf](http://junegunn.kr/2015/04/browsing-chrome-history-with-fzf/) +- [Browsing Chrome bookmarks with fzf](http://junegunn.kr/2015/04/browsing-chrome-bookmarks-with-fzf/) Tips ---- @@ -384,13 +372,13 @@ Tips If you have any rendering issues, check the followings: 1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it - contains `256` (e.g. `xterm-256color`) + contains `256` (e.g. `xterm-256color`) 2. If you're on screen or tmux, `$TERM` should be either `screen` or - `screen-256color` + `screen-256color` 3. Some terminal emulators (e.g. mintty) have problem displaying default - background color and make some text unable to read. In that case, try `--black` - option. And if it solves your problem, I recommend including it in - `FZF_DEFAULT_OPTS` for further convenience. + background color and make some text unable to read. In that case, try + `--black` option. And if it solves your problem, I recommend including it + in `FZF_DEFAULT_OPTS` for further convenience. 4. If you still have problem, try `--no-256` option or even `--no-color`. #### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` @@ -421,41 +409,6 @@ export FZF_DEFAULT_COMMAND=' find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null' ``` -#### Using fzf with tmux panes - -The supplied [fzf-tmux](bin/fzf-tmux) script should suffice in most of the -cases, but if you want to be able to update command line like the default -`CTRL-T` key binding, you'll have to use `send-keys` command of tmux. The -following example will show you how it can be done. - -```sh -# This is a helper function that splits the current pane to start the given -# command ($1) and sends its output back to the original pane with any number of -# optional keys (shift; $*). -fzf_tmux_helper() { - [ -n "$TMUX_PANE" ] || return - local cmd=$1 - shift - tmux split-window -p 40 \ - "bash -c \"\$(tmux send-keys -t $TMUX_PANE \"\$(source ~/.fzf.bash; $cmd)\" $*)\"" -} - -# This is the function we are going to run in the split pane. -# - "find" to list the directories -# - "sed" will escape spaces in the paths. -# - "paste" will join the selected paths into a single line -fzf_tmux_dir() { - fzf_tmux_helper \ - 'find * -path "*/\.*" -prune -o -type d -print 2> /dev/null | - fzf --multi | - sed "s/ /\\\\ /g" | - paste -sd" " -' Space -} - -# Bind CTRL-X-CTRL-D to fzf_tmux_dir -bind '"\C-x\C-d": "$(fzf_tmux_dir)\e\C-e"' -``` - #### Fish shell It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362) @@ -464,19 +417,7 @@ simple `vim (fzf)` won't work as expected. The workaround is to store the result of fzf to a temporary file. ```sh -function vimf - if fzf > $TMPDIR/fzf.result - vim (cat $TMPDIR/fzf.result) - end -end - -function fe - set tmp $TMPDIR/fzf.result - fzf --query="$argv[1]" --select-1 --exit-0 > $tmp - if [ (cat $tmp | wc -l) -gt 0 ] - vim (cat $tmp) - end -end +fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result) ``` #### Handling UTF-8 NFD paths on OSX From f4fe93338beda6b7410db3c790829e1d664f7180 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Apr 2015 02:09:16 +0900 Subject: [PATCH 224/408] Update README --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index a3157b9..7b54b3f 100644 --- a/README.md +++ b/README.md @@ -357,13 +357,6 @@ nnoremap :call fzf#run({ More examples can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). -#### Articles - -- [fzf+vim+tmux](http://junegunn.kr/2014/04/fzf+vim+tmux) -- [Browsing git commits with fzf](http://junegunn.kr/2015/03/browsing-git-commits-with-fzf/) -- [Browsing Chrome history with fzf](http://junegunn.kr/2015/04/browsing-chrome-history-with-fzf/) -- [Browsing Chrome bookmarks with fzf](http://junegunn.kr/2015/04/browsing-chrome-bookmarks-with-fzf/) - Tips ---- From 305ec3b3ce16141ecac0b8657ad00b371fc7a133 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Apr 2015 14:33:03 +0900 Subject: [PATCH 225/408] [fish] Remove buffering delay by not using subroutines Close #169 --- shell/key-bindings.fish | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 8be5ab5..36beee1 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -7,18 +7,6 @@ function fzf_key_bindings set -g TMPDIR /tmp end - function __fzf_list - command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ - -o -type f -print \ - -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3- - end - - function __fzf_list_dir - command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \ - -prune -o -type d -print 2> /dev/null | sed 1d | cut -b3- - end - function __fzf_escape while read item echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' ' @@ -26,7 +14,10 @@ function fzf_key_bindings end function __fzf_ctrl_t - __fzf_list | eval (__fzfcmd) -m > $TMPDIR/fzf.result + command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + -o -type f -print \ + -o -type d -print \ + -o -type l -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) -m > $TMPDIR/fzf.result and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) commandline -f repaint rm -f $TMPDIR/fzf.result @@ -41,7 +32,8 @@ function fzf_key_bindings function __fzf_alt_c # Fish hangs if the command before pipe redirects (2> /dev/null) - __fzf_list_dir | eval (__fzfcmd) +m > $TMPDIR/fzf.result + command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \ + -prune -o -type d -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) +m > $TMPDIR/fzf.result [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] and cd (cat $TMPDIR/fzf.result) commandline -f repaint From 27e26bd1ea90c54555aceecb644b390ced5d9761 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Apr 2015 12:51:08 +0900 Subject: [PATCH 226/408] [vim] Add g:Fzf_launcher for funcrefs (#212) --- README.md | 3 ++- plugin/fzf.vim | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7b54b3f..e7288b1 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,8 @@ of the selected items. | `dir` | string | Working directory | | `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | | `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) | -| `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) | +| `launcher` | string | External terminal emulator to start fzf with (GVim only) | +| `launcher` | funcref | Function for generating `launcher` string (GVim only) | _However on Neovim `fzf#run` is asynchronous and does not return values so you should use `sink` or `sink*` to process the output from fzf._ diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 28efe69..375d5ec 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -206,8 +206,9 @@ function! s:execute(dict, command, temps) call s:pushd(a:dict) silent! !clear 2> /dev/null if has('gui_running') - let launcher = get(a:dict, 'launcher', get(g:, 'fzf_launcher', s:launcher)) - let command = printf(launcher, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'") + let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) + let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher + let command = printf(fmt, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'") else let command = a:command endif From 9310ae28abb733d95f5c5be693e490601661f6d6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Apr 2015 19:29:01 +0900 Subject: [PATCH 227/408] [vim] Redraw screen after running fzf on tmux pane (#213) --- plugin/fzf.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 375d5ec..bb0b10b 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -234,6 +234,7 @@ function! s:execute_tmux(dict, command, temps) endif call system(command) + redraw! return s:callback(a:dict, a:temps) endfunction From 857619995ef54d66141fc4de882481fc6c44d186 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Apr 2015 19:29:42 +0900 Subject: [PATCH 228/408] [vim] Ignore E325 (#213) --- plugin/fzf.vim | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index bb0b10b..8301cb9 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -300,6 +300,7 @@ function! s:execute_term(dict, command, temps) endfunction function! s:callback(dict, temps) +try if !filereadable(a:temps.result) let lines = [] else @@ -323,6 +324,11 @@ function! s:callback(dict, temps) endfor return lines +catch + if stridx(v:exception, ':E325:') < 0 + echoerr v:exception + endif +endtry endfunction let s:default_action = { From 6ed9de9051660dff636a14d86ad31cc25973a04c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Apr 2015 22:31:23 +0900 Subject: [PATCH 229/408] [zsh] Temporarily unset no_bang_hist for CTRL-R Close #214 --- shell/key-bindings.zsh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index f8cbb71..451e418 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -35,11 +35,16 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { - local selected + local selected restore_no_bang_hist if selected=$(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') LBUFFER=!$num + if setopt | grep nobanghist > /dev/null; then + restore_no_bang_hist=1 + unsetopt no_bang_hist + fi zle expand-history + [ -n "$restore_no_bang_hist" ] && setopt no_bang_hist fi zle redisplay } From 36924d0b1c7c8f01739316ed6e68c78c042e3973 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Apr 2015 22:39:07 +0900 Subject: [PATCH 230/408] [zsh] Do not change LBUFFER on empty selection (CTRL-R) --- shell/key-bindings.zsh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 451e418..1760867 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -38,13 +38,15 @@ fzf-history-widget() { local selected restore_no_bang_hist if selected=$(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') - LBUFFER=!$num - if setopt | grep nobanghist > /dev/null; then - restore_no_bang_hist=1 - unsetopt no_bang_hist + if [ -n "$num" ]; then + LBUFFER=!$num + if setopt | grep nobanghist > /dev/null; then + restore_no_bang_hist=1 + unsetopt no_bang_hist + fi + zle expand-history + [ -n "$restore_no_bang_hist" ] && setopt no_bang_hist fi - zle expand-history - [ -n "$restore_no_bang_hist" ] && setopt no_bang_hist fi zle redisplay } From 22ae7adac811ad5380718b29a1d7325bddf2cc6c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Apr 2015 22:43:48 +0900 Subject: [PATCH 231/408] Update completion for fzf itself --- shell/completion.bash | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 62dc140..7916098 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -31,11 +31,11 @@ _fzf_opts_completion() { --tiebreak -m --multi --no-mouse - +c --no-color - +2 --no-256 + --color --black --reverse --no-hscroll + --inline-info --prompt -q --query -1 --select-1 @@ -51,6 +51,10 @@ _fzf_opts_completion() { COMPREPLY=( $(compgen -W "length begin end index" -- ${cur}) ) return 0 ;; + --color) + COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) ) + return 0 + ;; esac if [[ ${cur} =~ ^-|\+ ]]; then From 09d0ac03471db4b8b69c9d8b2dab83548e9b9c3c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 24 Apr 2015 12:45:39 +0900 Subject: [PATCH 232/408] [vim] Update default launcher for GVim (#212) Code submitted by @lydell --- plugin/fzf.vim | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 8301cb9..85f576d 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -22,7 +22,6 @@ " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. let s:default_height = '40%' -let s:launcher = 'xterm -e bash -ic %s' let s:fzf_go = expand(':h:h').'/bin/fzf' let s:install = expand(':h:h').'/install' let s:installed = 0 @@ -202,6 +201,15 @@ function! s:popd(dict) endif endfunction +function! s:xterm_launcher() + return printf('xterm -T [fzf]' + \ .' -bg "\%s" -fg "\%s"' + \ .' -geometry %dx%d+%d+%d -e bash -ic %%s', + \ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"), + \ &columns, &lines/2, getwinposx(), getwinposy()) +endfunction +let s:launcher = function('s:xterm_launcher') + function! s:execute(dict, command, temps) call s:pushd(a:dict) silent! !clear 2> /dev/null From 9c6e46ab154353f31a1f881a70ac78695b3509bb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 24 Apr 2015 12:54:57 +0900 Subject: [PATCH 233/408] [fzf-tmux] Fix #215 - Prepend env to avoid error on fish --- bin/fzf-tmux | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index fe9f8ad..c723288 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -107,7 +107,7 @@ fail() { fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" [ -x "$fzf" ] || fail "fzf executable not found" -envs="" +envs="env " [ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" From 5794fd42df0dc296a869add78807ef1e14f6d2ce Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 25 Apr 2015 01:09:05 +0900 Subject: [PATCH 234/408] Fix test code --- test/test_go.rb | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 69285c8..9a7055d 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -130,16 +130,26 @@ class Tmux def until opts = {} lines = nil wait(opts) do - yield lines = capture(opts) + lines = capture(opts) + class << lines + def item_count + self[-2] ? self[-2].strip.split('/').last.to_i : 0 + end + end + yield lines end lines end def prepare - self.send_keys 'echo hello', :Enter - self.until { |lines| lines[-1].start_with?('hello') } - self.send_keys 'clear', :Enter - self.until { |lines| lines.empty? } + tries = 0 + begin + self.send_keys 'C-u', 'hello' + self.until { |lines| lines[-1].end_with?('hello') } + rescue Exception + (tries += 1) < 5 ? retry : raise + end + self.send_keys 'C-u' end private def defaults opts @@ -389,7 +399,7 @@ class TestGoFZF < TestBase def test_query_unicode tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter - tmux.until { |lines| lines.last.start_with? '>' } + tmux.until { |lines| lines[-2].include? '1/2' } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include?(FIN) } assert_equal ['가나다'], readonce.split($/) @@ -564,7 +574,7 @@ module TestShell def test_ctrl_t tmux.prepare tmux.send_keys 'C-t', pane: 0 - lines = tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } + lines = tmux.until(pane: 1) { |lines| lines.item_count > 0 } expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') tmux.send_keys :BTab, :BTab, :Enter, pane: 1 tmux.until(pane: 0) { |lines| lines[-1].include? expected } @@ -573,7 +583,7 @@ module TestShell # FZF_TMUX=0 new_shell tmux.send_keys 'C-t', pane: 0 - lines = tmux.until(pane: 0) { |lines| lines[-1].start_with? '>' } + lines = tmux.until(pane: 0) { |lines| lines.item_count > 0 } expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') tmux.send_keys :BTab, :BTab, :Enter, pane: 0 tmux.until(pane: 0) { |lines| lines[-1].include? expected } @@ -583,7 +593,7 @@ module TestShell def test_alt_c tmux.prepare tmux.send_keys :Escape, :c, pane: 0 - lines = tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } + lines = tmux.until(pane: 1) { |lines| lines.item_count > 0 } expected = lines[-3][2..-1] p expected tmux.send_keys :Enter, pane: 1 @@ -600,7 +610,7 @@ module TestShell tmux.send_keys 'echo 3rd', :Enter; tmux.prepare tmux.send_keys 'echo 4th', :Enter; tmux.prepare tmux.send_keys 'C-r', pane: 0 - tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } + tmux.until(pane: 1) { |lines| lines.item_count > 0 } tmux.send_keys '3d', pane: 1 tmux.until(pane: 1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort tmux.send_keys :Enter, pane: 1 @@ -627,7 +637,7 @@ class TestBash < TestBase tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter tmux.prepare tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 - tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } + tmux.until(pane: 1) { |lines| lines.item_count > 0 } tmux.send_keys :BTab, :BTab, :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -640,7 +650,7 @@ class TestBash < TestBase tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter tmux.prepare tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0 - tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } + tmux.until(pane: 1) { |lines| lines.item_count > 0 } tmux.send_keys :BTab, :BTab # BTab does not work here tmux.send_keys 55 tmux.until(pane: 1) { |lines| lines[-2].start_with? ' 1/' } @@ -669,7 +679,7 @@ class TestBash < TestBase pid = lines[-1].split.last tmux.prepare tmux.send_keys 'kill ', :Tab, pane: 0 - tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } + tmux.until(pane: 1) { |lines| lines.item_count > 0 } tmux.send_keys 'sleep12345' tmux.until(pane: 1) { |lines| lines[-3].include? 'sleep 12345' } tmux.send_keys :Enter From 5502b68a1de42d4e23ba2192f900e5a7157d8900 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 25 Apr 2015 10:39:04 +0900 Subject: [PATCH 235/408] Test refactoring --- test/test_go.rb | 136 +++++++++++++++--------------------------------- 1 file changed, 42 insertions(+), 94 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 9a7055d..c9ca346 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -4,6 +4,8 @@ require 'minitest/autorun' require 'fileutils' +DEFAULT_TIMEOUT = 20 + base = File.expand_path('../../', __FILE__) Dir.chdir base FZF = "#{base}/bin/fzf" @@ -22,26 +24,13 @@ class NilClass end end -module Temp - def readonce - name = self.class::TEMPNAME - waited = 0 - while waited < 5 - begin - system 'sync' - 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 +def wait + since = Time.now + while Time.now - since < DEFAULT_TIMEOUT + return if yield + sleep 0.05 end + throw 'timeout' end class Shell @@ -59,8 +48,6 @@ class Shell end class Tmux - include Temp - TEMPNAME = '/tmp/fzf-test.txt' attr_reader :win @@ -111,26 +98,19 @@ class Tmux go("send-keys -t #{target} #{args}") end - def capture opts = {} - timeout, pane = defaults(opts).values_at(:timeout, :pane) - waited = 0 - loop do - go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME}") - break if $?.exitstatus == 0 - - if waited > timeout - raise "Window not found" - end - waited += 0.1 - sleep 0.1 + def capture pane = 0 + File.unlink TEMPNAME while File.exists? TEMPNAME + wait do + go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME} 2> /dev/null") + $?.exitstatus == 0 end - readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse + File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse end - def until opts = {} + def until pane = 0 lines = nil - wait(opts) do - lines = capture(opts) + wait do + lines = capture(pane) class << lines def item_count self[-2] ? self[-2].strip.split('/').last.to_i : 0 @@ -152,37 +132,12 @@ class Tmux self.send_keys 'C-u' end private - def defaults opts - { timeout: 10, pane: 0 }.merge(opts) - end - - def wait opts = {} - timeout, pane = defaults(opts).values_at(:timeout, :pane) - waited = 0 - until yield - if waited > timeout - hl = '=' * 10 - puts hl - capture(opts).each_with_index do |line, idx| - puts [idx.to_s.rjust(2), line].join(': ') - end - puts hl - raise "timeout" - end - waited += 0.1 - sleep 0.1 - end - end - def go *args %x[tmux #{args.join ' '}].split($/) end end class TestBase < Minitest::Test - include Temp - - FIN = 'FIN' TEMPNAME = '/tmp/output' attr_reader :tmux @@ -190,10 +145,18 @@ class TestBase < Minitest::Test def setup ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_COMMAND' + File.unlink TEMPNAME while File.exists?(TEMPNAME) + end + + def readonce + wait { File.exists?(TEMPNAME) } + File.read(TEMPNAME) + ensure + File.unlink TEMPNAME while File.exists?(TEMPNAME) end def fzf(*opts) - fzf!(*opts) + " > #{TEMPNAME} && echo #{FIN}" + fzf!(*opts) + " > #{TEMPNAME}.tmp; mv #{TEMPNAME}.tmp #{TEMPNAME}" end def fzf!(*opts) @@ -224,8 +187,7 @@ class TestGoFZF < TestBase def test_vanilla tmux.send_keys "seq 1 100000 | #{fzf}", :Enter - tmux.until(timeout: 20) { |lines| - lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } + tmux.until { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } lines = tmux.capture assert_equal ' 2', lines[-4] assert_equal '> 1', lines[-3] @@ -330,7 +292,6 @@ class TestGoFZF < TestBase :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?(FIN) } assert_equal %w[3 2 5 6 8 7], readonce.split($/) tmux.close end @@ -351,13 +312,11 @@ class TestGoFZF < TestBase # However, the output must not be transformed if multi tmux.send_keys :BTab, :BTab, :Enter - 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?(FIN) } assert_equal [' 1st 2nd 3rd/'], readonce.split($/) end end @@ -370,20 +329,17 @@ class TestGoFZF < TestBase 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?(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'}", :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'}", :Enter - tmux.until { |lines| lines[-1].include?(FIN) } assert_equal ['555555'], readonce.split($/) end @@ -392,7 +348,6 @@ class TestGoFZF < TestBase tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter tmux.until { |lines| lines.last =~ /^> 5/ } tmux.send_keys :BTab, :BTab, :BTab, :Enter - tmux.until { |lines| lines[-1].include?(FIN) } assert_equal ['5', '5', '15', '25'], readonce.split($/) end end @@ -401,7 +356,6 @@ class TestGoFZF < TestBase tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter tmux.until { |lines| lines[-2].include? '1/2' } tmux.send_keys :Enter - tmux.until { |lines| lines[-1].include?(FIN) } assert_equal ['가나다'], readonce.split($/) end @@ -550,15 +504,9 @@ class TestGoFZF < TestBase end private - def writelines path, lines, timeout = 10 - File.open(path, 'w') do |f| - f << lines.join($/) - f.sync - end - since = Time.now - while `cat #{path}`.split($/).length != lines.length && (Time.now - since) < 10 - sleep 0.1 - end + def writelines path, lines + File.unlink path while File.exists? path + File.open(path, 'w') { |f| f << lines.join($/) } end end @@ -574,26 +522,26 @@ module TestShell def test_ctrl_t tmux.prepare tmux.send_keys 'C-t', pane: 0 - lines = tmux.until(pane: 1) { |lines| lines.item_count > 0 } + lines = tmux.until(1) { |lines| lines.item_count > 0 } expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') tmux.send_keys :BTab, :BTab, :Enter, pane: 1 - tmux.until(pane: 0) { |lines| lines[-1].include? expected } + tmux.until(0) { |lines| lines[-1].include? expected } tmux.send_keys 'C-c' # FZF_TMUX=0 new_shell tmux.send_keys 'C-t', pane: 0 - lines = tmux.until(pane: 0) { |lines| lines.item_count > 0 } + lines = tmux.until(0) { |lines| lines.item_count > 0 } expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') tmux.send_keys :BTab, :BTab, :Enter, pane: 0 - tmux.until(pane: 0) { |lines| lines[-1].include? expected } + tmux.until(0) { |lines| lines[-1].include? expected } tmux.send_keys 'C-c', 'C-d' end def test_alt_c tmux.prepare tmux.send_keys :Escape, :c, pane: 0 - lines = tmux.until(pane: 1) { |lines| lines.item_count > 0 } + lines = tmux.until(1) { |lines| lines.item_count > 0 } expected = lines[-3][2..-1] p expected tmux.send_keys :Enter, pane: 1 @@ -610,9 +558,9 @@ module TestShell tmux.send_keys 'echo 3rd', :Enter; tmux.prepare tmux.send_keys 'echo 4th', :Enter; tmux.prepare tmux.send_keys 'C-r', pane: 0 - tmux.until(pane: 1) { |lines| lines.item_count > 0 } + tmux.until(1) { |lines| lines.item_count > 0 } tmux.send_keys '3d', pane: 1 - tmux.until(pane: 1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort + tmux.until(1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort tmux.send_keys :Enter, pane: 1 tmux.until { |lines| lines[-1] == 'echo 3rd' } tmux.send_keys :Enter @@ -637,7 +585,7 @@ class TestBash < TestBase tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter tmux.prepare tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 - tmux.until(pane: 1) { |lines| lines.item_count > 0 } + tmux.until(1) { |lines| lines.item_count > 0 } tmux.send_keys :BTab, :BTab, :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -650,10 +598,10 @@ class TestBash < TestBase tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter tmux.prepare tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0 - tmux.until(pane: 1) { |lines| lines.item_count > 0 } + tmux.until(1) { |lines| lines.item_count > 0 } tmux.send_keys :BTab, :BTab # BTab does not work here tmux.send_keys 55 - tmux.until(pane: 1) { |lines| lines[-2].start_with? ' 1/' } + tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -679,9 +627,9 @@ class TestBash < TestBase pid = lines[-1].split.last tmux.prepare tmux.send_keys 'kill ', :Tab, pane: 0 - tmux.until(pane: 1) { |lines| lines.item_count > 0 } + tmux.until(1) { |lines| lines.item_count > 0 } tmux.send_keys 'sleep12345' - tmux.until(pane: 1) { |lines| lines[-3].include? 'sleep 12345' } + tmux.until(1) { |lines| lines[-3].include? 'sleep 12345' } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' From 4b7c571575a2882ef99c43c233c513c9959fbca5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 25 Apr 2015 10:54:47 +0900 Subject: [PATCH 236/408] Fix race condition in test case --- test/test_go.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index c9ca346..061a596 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -389,6 +389,7 @@ class TestGoFZF < TestBase tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.send_keys '00' + tmux.until { |lines| lines[-2].include? '10/1000' } tmux.send_keys :BTab, :BTab, :BTab, :Enter assert_equal %w[1000 900 800], readonce.split($/) end @@ -398,6 +399,7 @@ class TestGoFZF < TestBase tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys '55' + tmux.until { |lines| lines[-2].include? '1/100' } tmux.send_keys *feed assert_equal [expected, '55'], readonce.split($/) end @@ -416,6 +418,7 @@ class TestGoFZF < TestBase tmux.send_keys "seq 1 100 | #{fzf '--expect=alt-z', :print_query}", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys '55' + tmux.until { |lines| lines[-2].include? '1/100' } tmux.send_keys :Escape, :z assert_equal ['55', 'alt-z', '55'], readonce.split($/) end @@ -543,11 +546,10 @@ module TestShell tmux.send_keys :Escape, :c, pane: 0 lines = tmux.until(1) { |lines| lines.item_count > 0 } expected = lines[-3][2..-1] - p expected tmux.send_keys :Enter, pane: 1 tmux.prepare tmux.send_keys :pwd, :Enter - tmux.until { |lines| p lines; lines[-1].end_with?(expected) } + tmux.until { |lines| lines[-1].end_with?(expected) } end def test_ctrl_r From 5d9107fd15a5bca3973f8206884ddfa66d47b923 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 25 Apr 2015 23:20:40 +0900 Subject: [PATCH 237/408] Print info after prompt on redraw This fixes the issue where "inline-info" is not immediately rendered when the terminal is resized. --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index cae8bf7..c38d714 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -436,8 +436,8 @@ func processTabs(runes []rune, prefixWidth int) (string, int) { func (t *Terminal) printAll() { t.printList() - t.printInfo() t.printPrompt() + t.printInfo() } func (t *Terminal) refresh() { From 2e339e49b85fd365e6e393222b8a29f9a99d1ed8 Mon Sep 17 00:00:00 2001 From: Oliver Schrenk Date: Mon, 30 Mar 2015 23:10:47 +0200 Subject: [PATCH 238/408] Support for vi insert mode in upcoming fish 2.2.0 --- shell/key-bindings.fish | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index ce1eea7..3a86dec 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -76,5 +76,11 @@ function fzf_key_bindings bind \ct '__fzf_ctrl_t' bind \cr '__fzf_ctrl_r' bind \ec '__fzf_alt_c' + + if bind -M insert > /dev/null 2>&1 + bind -M insert \ct '__fzf_ctrl_t' + bind -M insert \cr '__fzf_ctrl_r' + bind -M insert \ec '__fzf_alt_c' + end end From 3d26b5336c964164b42dc698d7f3bd927368c90e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 28 Apr 2015 23:49:52 +0900 Subject: [PATCH 239/408] [vim] Fix #220 - Prevent error after update --- plugin/fzf.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 85f576d..ffb0e2c 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -208,6 +208,7 @@ function! s:xterm_launcher() \ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"), \ &columns, &lines/2, getwinposx(), getwinposy()) endfunction +unlet! s:launcher let s:launcher = function('s:xterm_launcher') function! s:execute(dict, command, temps) From 0937bd6c166c9d8be1fdafa8535460cdd0b28620 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 3 May 2015 00:58:45 +0900 Subject: [PATCH 240/408] [vim] Improve binary detection /cc @alerque - Ask for user confirmation before running `install --bin` - Removed `s:fzf_rb` since `install --bin` will create a wrapper executable that just runs Ruby version on the platforms where prebuilt binaries are not available. --- plugin/fzf.vim | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index ffb0e2c..68c9c9e 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -25,7 +25,6 @@ let s:default_height = '40%' let s:fzf_go = expand(':h:h').'/bin/fzf' let s:install = expand(':h:h').'/install' let s:installed = 0 -let s:fzf_rb = expand(':h:h').'/fzf' let s:fzf_tmux = expand(':h:h').'/bin/fzf-tmux' let s:cpo_save = &cpo @@ -35,32 +34,24 @@ function! s:fzf_exec() if !exists('s:exec') if executable(s:fzf_go) let s:exec = s:fzf_go + elseif executable('fzf') + let s:exec = 'fzf' + elseif !s:installed && executable(s:install) && + \ input('fzf executable not found. Download binary? (y/n) ') =~? '^y' + redraw + echo + echohl WarningMsg + echo 'Downloading fzf binary. Please wait ...' + echohl None + let s:installed = 1 + call system(s:install.' --bin') + return s:fzf_exec() else - let path = split(system('which fzf 2> /dev/null'), '\n') - if !v:shell_error && !empty(path) - let s:exec = path[0] - elseif !s:installed && executable(s:install) - echohl WarningMsg - echo 'Downloading fzf binary. Please wait ...' - echohl None - let s:installed = 1 - call system(s:install.' --bin') - return s:fzf_exec() - elseif executable(s:fzf_rb) - let s:exec = s:fzf_rb - else - call system('type fzf') - if v:shell_error - throw 'fzf executable not found' - else - let s:exec = 'fzf' - endif - endif + redraw + throw 'fzf executable not found' endif - return s:exec - else - return s:exec endif + return s:exec endfunction function! s:tmux_enabled() From ed8202efc6861e0ae5b921819b86d2ad22c35a65 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 8 May 2015 18:16:55 +0900 Subject: [PATCH 241/408] [bash-completion] Ignore 0.0.0.0 Close #228 --- shell/completion.bash | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 7916098..990852b 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -183,13 +183,13 @@ _fzf_kill_completion() { _fzf_telnet_completion() { _fzf_list_completion '+m' "$@" << "EOF" - \grep -v '^\s*\(#\|$\)' /etc/hosts | awk '{if (length($2) > 0) {print $2}}' | sort -u + \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u EOF } _fzf_ssh_completion() { _fzf_list_completion '+m' "$@" << "EOF" - cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i ^host | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts) | awk '{print $2}' | sort -u + cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i ^host | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u EOF } From 65a2bdb01de320151d92a291021728ea751c0f44 Mon Sep 17 00:00:00 2001 From: James Sully Date: Sat, 9 May 2015 01:26:34 +1000 Subject: [PATCH 242/408] fix typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e7288b1..eb5ecb5 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ fzf is a general-purpose command-line fuzzy finder. Pros ---- -- No dependency +- No dependencies - Blazingly fast - e.g. `locate / | fzf` - Flexible layout From 2b8e44532163b8ef71a805725e8d17e352cba824 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 9 May 2015 20:15:14 +0900 Subject: [PATCH 243/408] Fuzzy completion for zsh (#227) --- README.md | 6 +- install | 2 +- shell/completion.zsh | 162 +++++++++++++++++++++++++++++++++++++++++++ test/test_go.rb | 38 +++++----- 4 files changed, 188 insertions(+), 20 deletions(-) create mode 100644 shell/completion.zsh diff --git a/README.md b/README.md index eb5ecb5..c5dd3f4 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ fzf project consists of the followings: - `fzf-tmux` script for launching fzf in a tmux pane - Shell extensions - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) - - Fuzzy auto-completion (bash only) + - Fuzzy auto-completion (bash, zsh) - Vim/Neovim plugin You can [download fzf executable][bin] alone, but it's recommended that you @@ -173,8 +173,8 @@ cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse It will still work even when you're not on tmux, silently ignoring `-[udlr]` options, so you can invariably use `fzf-tmux` in your scripts. -Fuzzy completion for bash -------------------------- +Fuzzy completion for bash and zsh +--------------------------------- #### Files and directories diff --git a/install b/install index ea89ee6..efe9b6a 100755 --- a/install +++ b/install @@ -177,7 +177,7 @@ for shell in bash zsh; do src=~/.fzf.${shell} fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\"" - if [ $shell != bash -o $auto_completion -ne 0 ]; then + if [ $auto_completion -ne 0 ]; then fzf_completion="# $fzf_completion" fi diff --git a/shell/completion.zsh b/shell/completion.zsh new file mode 100644 index 0000000..1d49915 --- /dev/null +++ b/shell/completion.zsh @@ -0,0 +1,162 @@ +#!/bin/zsh +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/-completion.zsh +# +# - $FZF_TMUX (default: 1) +# - $FZF_TMUX_HEIGHT (default: '40%') +# - $FZF_COMPLETION_TRIGGER (default: '**') +# - $FZF_COMPLETION_OPTS (default: empty) + +_fzf_path_completion() { + local base lbuf find_opts fzf_opts suffix tail fzf dir leftover matches + base=$1 + lbuf=$2 + find_opts=$3 + fzf_opts=$4 + suffix=$5 + tail=$6 + [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + + dir="$base" + while [ 1 ]; do + if [ -z "$dir" -o -d ${~dir} ]; then + leftover=${base/#"$dir"} + leftover=${leftover/#\/} + [ "$dir" = './' ] && dir='' + matches=$(find -L ${~dir}* ${=find_opts} 2> /dev/null | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do + printf "%q$suffix " "$item" + done) + matches=${matches% } + if [ -n "$matches" ]; then + LBUFFER="$lbuf$matches$tail" + zle redisplay + fi + return + fi + dir=$(dirname "$dir") + [[ "$dir" =~ /$ ]] || dir="$dir"/ + done +} + +_fzf_all_completion() { + _fzf_path_completion "$1" "$2" \ + "-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \ + "-m" "" " " +} + +_fzf_file_completion() { + _fzf_path_completion "$1" "$2" \ + "-name .git -prune -o -name .svn -prune -o -type f -print -o -type l -print" \ + "-m" "" " " +} + +_fzf_dir_completion() { + _fzf_path_completion "$1" "$2" \ + "-name .git -prune -o -name .svn -prune -o -type d -print" \ + "" "/" "" +} + +_fzf_list_completion() { + local prefix lbuf fzf_opts src fzf matches + prefix=$1 + lbuf=$2 + fzf_opts=$3 + read -r src + [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + + matches=$(eval "$src" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$prefix") + if [ -n "$matches" ]; then + LBUFFER="$lbuf$matches " + zle redisplay + fi +} + +_fzf_telnet_completion() { + _fzf_list_completion "$1" "$2" '+m' << "EOF" + \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u +EOF +} + +_fzf_ssh_completion() { + _fzf_list_completion "$1" "$2" '+m' << "EOF" + cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i ^host | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u +EOF +} + +_fzf_env_var_completion() { + _fzf_list_completion "$1" "$2" '+m' << "EOF" + declare -xp | sed 's/=.*//' | sed 's/.* //' +EOF +} + +_fzf_alias_completion() { + _fzf_list_completion "$1" "$2" '+m' << "EOF" + alias | sed 's/=.*//' +EOF +} + +fzf-zsh-completion() { + local tokens cmd prefix trigger tail fzf matches lbuf d_cmds f_cmds a_cmds + + # http://zsh.sourceforge.net/FAQ/zshfaq03.html + tokens=(${=LBUFFER}) + if [ ${#tokens} -lt 1 ]; then + zle expand-or-complete + return + fi + + cmd=${tokens[1]} + trigger=${FZF_COMPLETION_TRIGGER:-**} + + # Trigger sequence given + tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} + if [ ${#tokens} -gt 1 -a $tail = $trigger ]; then + d_cmds=(cd pushd rmdir) + f_cmds=( + awk cat diff diff3 + emacs ex file ftp g++ gcc gvim head hg java + javac ld less more mvim patch perl python ruby + sed sftp sort source tail tee uniq vi view vim wc) + a_cmds=( + basename bunzip2 bzip2 chmod chown curl cp dirname du + find git grep gunzip gzip hg jar + ln ls mv open rm rsync scp + svn tar unzip zip) + + prefix=${tokens[-1]:0:-${#trigger}} + lbuf=${LBUFFER:0:-${#tokens[-1]}} + if [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then + _fzf_dir_completion "$prefix" $lbuf + elif [ ${f_cmds[(i)$cmd]} -le ${#f_cmds} ]; then + _fzf_file_completion "$prefix" $lbuf + elif [ ${a_cmds[(i)$cmd]} -le ${#a_cmds} ]; then + _fzf_all_completion "$prefix" $lbuf + elif [ $cmd = telnet ]; then + _fzf_telnet_completion "$prefix" $lbuf + elif [ $cmd = ssh ]; then + _fzf_ssh_completion "$prefix" $lbuf + elif [ $cmd = unset -o $cmd = export ]; then + _fzf_env_var_completion "$prefix" $lbuf + elif [ $cmd = unalias ]; then + _fzf_alias_completion "$prefix" $lbuf + fi + # Kill completion (do not require trigger sequence) + elif [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then + [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ') + if [ -n "$matches" ]; then + LBUFFER="$LBUFFER$matches" + zle redisplay + fi + # Fall back to default completion + else + zle expand-or-complete + fi +} + +zle -N fzf-zsh-completion +bindkey '^I' fzf-zsh-completion + diff --git a/test/test_go.rb b/test/test_go.rb index 061a596..d252c00 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -570,19 +570,7 @@ module TestShell end end -class TestBash < TestBase - include TestShell - - def new_shell - tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter - tmux.prepare - end - - def setup - super - @tmux = Tmux.new :bash - end - +module CompletionTest def test_file_completion tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter tmux.prepare @@ -612,9 +600,11 @@ class TestBash < TestBase tmux.send_keys :xx tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } - # Should not match regular files - tmux.send_keys :Tab - tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } + # Should not match regular files (bash-only) + if self.class == TestBash + tmux.send_keys :Tab + tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } + end # Fail back to plusdirs tmux.send_keys :BSpace, :BSpace, :BSpace @@ -640,8 +630,24 @@ class TestBash < TestBase end end +class TestBash < TestBase + include TestShell + include CompletionTest + + def new_shell + tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter + tmux.prepare + end + + def setup + super + @tmux = Tmux.new :bash + end +end + class TestZsh < TestBase include TestShell + include CompletionTest def new_shell tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter From 5a17a6323a208174896f0850ed545d297f0a23f8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 9 May 2015 20:36:25 +0900 Subject: [PATCH 244/408] [zsh-completion] "\find" to bypass alias --- shell/completion.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 1d49915..56636c4 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -26,7 +26,7 @@ _fzf_path_completion() { leftover=${base/#"$dir"} leftover=${leftover/#\/} [ "$dir" = './' ] && dir='' - matches=$(find -L ${~dir}* ${=find_opts} 2> /dev/null | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do + matches=$(\find -L ${~dir}* ${=find_opts} 2> /dev/null | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do printf "%q$suffix " "$item" done) matches=${matches% } From f4a595eeddd0a95d1b9cca83c90391ff7de30250 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 9 May 2015 20:42:13 +0900 Subject: [PATCH 245/408] Fix Travis CI build --- test/test_go.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index d252c00..ff4eb1d 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -77,8 +77,10 @@ class Tmux end def close - send_keys 'C-c', 'C-u', 'exit', :Enter - wait { closed? } + wait do + send_keys 'C-c', 'C-u', 'exit', :Enter + closed? + end end def kill From dc64568c83d23c7f53322aa7e619aa099a94dfc7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 9 May 2015 21:04:16 +0900 Subject: [PATCH 246/408] [zsh-completion] Completion for unknown commands --- shell/completion.zsh | 24 +++--------------------- test/test_go.rb | 2 +- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 56636c4..9537cc1 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -47,12 +47,6 @@ _fzf_all_completion() { "-m" "" " " } -_fzf_file_completion() { - _fzf_path_completion "$1" "$2" \ - "-name .git -prune -o -name .svn -prune -o -type f -print -o -type l -print" \ - "-m" "" " " -} - _fzf_dir_completion() { _fzf_path_completion "$1" "$2" \ "-name .git -prune -o -name .svn -prune -o -type d -print" \ @@ -99,7 +93,7 @@ EOF } fzf-zsh-completion() { - local tokens cmd prefix trigger tail fzf matches lbuf d_cmds f_cmds a_cmds + local tokens cmd prefix trigger tail fzf matches lbuf d_cmds # http://zsh.sourceforge.net/FAQ/zshfaq03.html tokens=(${=LBUFFER}) @@ -115,25 +109,11 @@ fzf-zsh-completion() { tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} if [ ${#tokens} -gt 1 -a $tail = $trigger ]; then d_cmds=(cd pushd rmdir) - f_cmds=( - awk cat diff diff3 - emacs ex file ftp g++ gcc gvim head hg java - javac ld less more mvim patch perl python ruby - sed sftp sort source tail tee uniq vi view vim wc) - a_cmds=( - basename bunzip2 bzip2 chmod chown curl cp dirname du - find git grep gunzip gzip hg jar - ln ls mv open rm rsync scp - svn tar unzip zip) prefix=${tokens[-1]:0:-${#trigger}} lbuf=${LBUFFER:0:-${#tokens[-1]}} if [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then _fzf_dir_completion "$prefix" $lbuf - elif [ ${f_cmds[(i)$cmd]} -le ${#f_cmds} ]; then - _fzf_file_completion "$prefix" $lbuf - elif [ ${a_cmds[(i)$cmd]} -le ${#a_cmds} ]; then - _fzf_all_completion "$prefix" $lbuf elif [ $cmd = telnet ]; then _fzf_telnet_completion "$prefix" $lbuf elif [ $cmd = ssh ]; then @@ -142,6 +122,8 @@ fzf-zsh-completion() { _fzf_env_var_completion "$prefix" $lbuf elif [ $cmd = unalias ]; then _fzf_alias_completion "$prefix" $lbuf + else + _fzf_all_completion "$prefix" $lbuf fi # Kill completion (do not require trigger sequence) elif [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then diff --git a/test/test_go.rb b/test/test_go.rb index ff4eb1d..94e6a72 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -126,7 +126,7 @@ class Tmux def prepare tries = 0 begin - self.send_keys 'C-u', 'hello' + self.send_keys 'C-u', 'hello', 'Right' self.until { |lines| lines[-1].end_with?('hello') } rescue Exception (tries += 1) < 5 ? retry : raise From 26a141c6a6222c73207babbc15f57fc7849d0f61 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 May 2015 02:35:00 +0900 Subject: [PATCH 247/408] [zsh-completion] Fix `~USERNAME**` (#230) --- shell/completion.zsh | 5 +++-- test/test_go.rb | 14 +++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 9537cc1..79a9b24 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -26,7 +26,8 @@ _fzf_path_completion() { leftover=${base/#"$dir"} leftover=${leftover/#\/} [ "$dir" = './' ] && dir='' - matches=$(\find -L ${~dir}* ${=find_opts} 2> /dev/null | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do + dir=${~dir} + matches=$(\find -L $dir* ${=find_opts} 2> /dev/null | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do printf "%q$suffix " "$item" done) matches=${matches% } @@ -37,7 +38,7 @@ _fzf_path_completion() { return fi dir=$(dirname "$dir") - [[ "$dir" =~ /$ ]] || dir="$dir"/ + dir=${dir%/}/ done } diff --git a/test/test_go.rb b/test/test_go.rb index 94e6a72..21299c4 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -574,7 +574,7 @@ end module CompletionTest def test_file_completion - tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter + tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}; touch ~/fzf-home', :Enter tmux.prepare tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 tmux.until(1) { |lines| lines.item_count > 0 } @@ -584,6 +584,18 @@ module CompletionTest lines[-1].include?('/tmp/fzf-test/10') && lines[-1].include?('/tmp/fzf-test/100') end + + # ~USERNAME** + tmux.send_keys 'C-u' + tmux.send_keys "cat ~#{ENV['USER']}**", :Tab, pane: 0 + tmux.until(1) { |lines| lines.item_count > 0 } + tmux.send_keys 'fzf-home' + tmux.until(1) { |lines| lines[-3].end_with? 'fzf-home' } + tmux.send_keys :Enter + tmux.until do |lines| + tmux.send_keys 'C-L' + lines[-1].include?('fzf-home') + end end def test_dir_completion From 207aa07891e480bb18a1bc790ea6d9560b24b696 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 May 2015 02:54:22 +0900 Subject: [PATCH 248/408] [zsh-completion] Temporarily set nonomatch (#230) No error on ~INVALID_USERNAME** --- shell/completion.zsh | 9 +++++++-- test/test_go.rb | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 79a9b24..c9c3276 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -11,7 +11,7 @@ # - $FZF_COMPLETION_OPTS (default: empty) _fzf_path_completion() { - local base lbuf find_opts fzf_opts suffix tail fzf dir leftover matches + local base lbuf find_opts fzf_opts suffix tail fzf dir leftover matches nnm base=$1 lbuf=$2 find_opts=$3 @@ -20,6 +20,10 @@ _fzf_path_completion() { tail=$6 [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + if ! setopt | grep nonomatch > /dev/null; then + nnm=1 + setopt nonomatch + fi dir="$base" while [ 1 ]; do if [ -z "$dir" -o -d ${~dir} ]; then @@ -35,11 +39,12 @@ _fzf_path_completion() { LBUFFER="$lbuf$matches$tail" zle redisplay fi - return + break fi dir=$(dirname "$dir") dir=${dir%/}/ done + [ -n "$nnm" ] && unsetopt nonomatch } _fzf_all_completion() { diff --git a/test/test_go.rb b/test/test_go.rb index 21299c4..7f25eb1 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -574,7 +574,7 @@ end module CompletionTest def test_file_completion - tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}; touch ~/fzf-home', :Enter + tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}; touch ~/fzf-home no~such~user', :Enter tmux.prepare tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 tmux.until(1) { |lines| lines.item_count > 0 } @@ -594,7 +594,17 @@ module CompletionTest tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' - lines[-1].include?('fzf-home') + lines[-1].end_with?('fzf-home') + end + + # ~INVALID_USERNAME** + tmux.send_keys 'C-u' + tmux.send_keys "cat ~such**", :Tab, pane: 0 + tmux.until(1) { |lines| lines[-3].end_with? 'no~such~user' } + tmux.send_keys :Enter + tmux.until do |lines| + tmux.send_keys 'C-L' + lines[-1].end_with?('no~such~user') end end From 683abb86efe910d656fc95005e74a3888b49541b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 May 2015 03:25:14 +0900 Subject: [PATCH 249/408] Dump screen content on test failure --- test/test_go.rb | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 7f25eb1..aa49a36 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -111,14 +111,22 @@ class Tmux def until pane = 0 lines = nil - wait do - lines = capture(pane) - class << lines - def item_count - self[-2] ? self[-2].strip.split('/').last.to_i : 0 + begin + wait do + lines = capture(pane) + class << lines + def item_count + self[-2] ? self[-2].strip.split('/').last.to_i : 0 + end end + yield lines end - yield lines + rescue Exception + puts $!.backtrace + puts '>' * 80 + puts lines + puts '<' * 80 + raise end lines end @@ -527,7 +535,7 @@ module TestShell def test_ctrl_t tmux.prepare tmux.send_keys 'C-t', pane: 0 - lines = tmux.until(1) { |lines| lines.item_count > 0 } + lines = tmux.until(1) { |lines| lines.item_count > 1 } expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') tmux.send_keys :BTab, :BTab, :Enter, pane: 1 tmux.until(0) { |lines| lines[-1].include? expected } @@ -536,7 +544,7 @@ module TestShell # FZF_TMUX=0 new_shell tmux.send_keys 'C-t', pane: 0 - lines = tmux.until(0) { |lines| lines.item_count > 0 } + lines = tmux.until(0) { |lines| lines.item_count > 1 } expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') tmux.send_keys :BTab, :BTab, :Enter, pane: 0 tmux.until(0) { |lines| lines[-1].include? expected } From 01405ad92ead8550d35073ff967cffc592a72665 Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Sat, 9 May 2015 21:11:01 +0200 Subject: [PATCH 250/408] fix typo in argument of head at least my version of head wants -n1 to only display the first line --- shell/key-bindings.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 1760867..86630d0 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -37,7 +37,7 @@ bindkey '\ec' fzf-cd-widget fzf-history-widget() { local selected restore_no_bang_hist if selected=$(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then - num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') + num=$(echo "$selected" | head -n1 | awk '{print $1}' | sed 's/[^0-9]//g') if [ -n "$num" ]; then LBUFFER=!$num if setopt | grep nobanghist > /dev/null; then From 5dc4df9570e2bd86e3e5f75e7b48943bfcfe6345 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 May 2015 05:01:52 +0900 Subject: [PATCH 251/408] Fix test cases --- test/test_go.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_go.rb b/test/test_go.rb index aa49a36..8c31c91 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -614,6 +614,8 @@ module CompletionTest tmux.send_keys 'C-L' lines[-1].end_with?('no~such~user') end + ensure + File.unlink 'no~such~user' end def test_dir_completion @@ -659,6 +661,7 @@ module CompletionTest tmux.send_keys 'C-L' lines[-1] == "kill #{pid}" end + tmux.send_keys :Enter end end From 732f1339400ee98c6b8c3af7bf6af1752e93fc53 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 May 2015 11:21:21 +0900 Subject: [PATCH 252/408] [test] Make sure to kill background process --- test/test_go.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 8c31c91..b6b37dc 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -661,7 +661,8 @@ module CompletionTest tmux.send_keys 'C-L' lines[-1] == "kill #{pid}" end - tmux.send_keys :Enter + ensure + Process.kill 'KILL', pid.to_i rescue nil if pid end end From 64949bf467d172accf2c060dce562ead736535aa Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 11 May 2015 10:17:33 +0900 Subject: [PATCH 253/408] [bash-completion] Allow specifying empty completion trigger (#230) --- shell/completion.bash | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 990852b..49781c0 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -87,7 +87,7 @@ _fzf_path_completion() { [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') COMPREPLY=() - trigger=${FZF_COMPLETION_TRIGGER:-**} + trigger=${FZF_COMPLETION_TRIGGER-'**'} cur="${COMP_WORDS[COMP_CWORD]}" if [[ ${cur} == *"$trigger" ]]; then base=${cur:0:${#cur}-${#trigger}} @@ -128,7 +128,7 @@ _fzf_list_completion() { [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" read -r src cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') - trigger=${FZF_COMPLETION_TRIGGER:-**} + trigger=${FZF_COMPLETION_TRIGGER-'**'} cur="${COMP_WORDS[COMP_CWORD]}" if [[ ${cur} == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} From 14fbe06d9e598b00d3dd05f7732f0e46ce205292 Mon Sep 17 00:00:00 2001 From: Tiziano Santoro Date: Mon, 11 May 2015 00:29:44 +0100 Subject: [PATCH 254/408] [zsh-completion] Allow specifying empty completion trigger. (#230) --- shell/completion.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index c9c3276..0e43d1c 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -109,7 +109,7 @@ fzf-zsh-completion() { fi cmd=${tokens[1]} - trigger=${FZF_COMPLETION_TRIGGER:-**} + trigger=${FZF_COMPLETION_TRIGGER-'**'} # Trigger sequence given tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} From 0cd238700caea82b44a1c66dc6a60323b83a1b68 Mon Sep 17 00:00:00 2001 From: Tiziano Santoro Date: Mon, 11 May 2015 00:44:07 +0100 Subject: [PATCH 255/408] [zsh-completion] Add comment clarifying trigger expansion. (#230) --- shell/completion.zsh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shell/completion.zsh b/shell/completion.zsh index 0e43d1c..a528554 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -109,6 +109,8 @@ fzf-zsh-completion() { fi cmd=${tokens[1]} + + # Explicitly allow for empty trigger. trigger=${FZF_COMPLETION_TRIGGER-'**'} # Trigger sequence given From 3b63b398101dbbcc8b09ba136a8e28a1dd6daa11 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 11 May 2015 13:06:02 +0900 Subject: [PATCH 256/408] [zsh-completion] Allow empty prefix & trigger sequence (#232) --- shell/completion.zsh | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index a528554..40e8f2e 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -112,14 +112,24 @@ fzf-zsh-completion() { # Explicitly allow for empty trigger. trigger=${FZF_COMPLETION_TRIGGER-'**'} + [ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("") - # Trigger sequence given tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} - if [ ${#tokens} -gt 1 -a $tail = $trigger ]; then + # Kill completion (do not require trigger sequence) + if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then + [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ') + if [ -n "$matches" ]; then + LBUFFER="$LBUFFER$matches" + zle redisplay + fi + # Trigger sequence given + elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then d_cmds=(cd pushd rmdir) - prefix=${tokens[-1]:0:-${#trigger}} - lbuf=${LBUFFER:0:-${#tokens[-1]}} + [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} + [ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}} + if [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then _fzf_dir_completion "$prefix" $lbuf elif [ $cmd = telnet ]; then @@ -133,14 +143,6 @@ fzf-zsh-completion() { else _fzf_all_completion "$prefix" $lbuf fi - # Kill completion (do not require trigger sequence) - elif [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then - [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ') - if [ -n "$matches" ]; then - LBUFFER="$LBUFFER$matches" - zle redisplay - fi # Fall back to default completion else zle expand-or-complete From 8f371ee81c4686a094dac9e8b9e48b75ddc648ee Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 11 May 2015 13:11:42 +0900 Subject: [PATCH 257/408] [zsh-completion] fzf-zsh-completion -> fzf-completion --- shell/completion.zsh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 40e8f2e..9dc4ce0 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -98,7 +98,7 @@ _fzf_alias_completion() { EOF } -fzf-zsh-completion() { +fzf-completion() { local tokens cmd prefix trigger tail fzf matches lbuf d_cmds # http://zsh.sourceforge.net/FAQ/zshfaq03.html @@ -149,6 +149,6 @@ fzf-zsh-completion() { fi } -zle -N fzf-zsh-completion -bindkey '^I' fzf-zsh-completion +zle -N fzf-completion +bindkey '^I' fzf-completion From c14aa99ef6c7514cae2aebaa2cbdd630cd1b7961 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 11 May 2015 16:59:44 +0900 Subject: [PATCH 258/408] [zsh/bash-completion] Avoid caret expansion Close #233 setopt extendedglob on zsh caused caret in grep pattern to be expanded. Problem identified and patch submitted by @lazywei. --- shell/completion.bash | 2 +- shell/completion.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 49781c0..ac8198e 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -189,7 +189,7 @@ EOF _fzf_ssh_completion() { _fzf_list_completion '+m' "$@" << "EOF" - cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i ^host | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u + cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u EOF } diff --git a/shell/completion.zsh b/shell/completion.zsh index 9dc4ce0..9035011 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -82,7 +82,7 @@ EOF _fzf_ssh_completion() { _fzf_list_completion "$1" "$2" '+m' << "EOF" - cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i ^host | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u + cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u EOF } From 0b770cd48a43cd817d9810213da0de2a295bb1a2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 11 May 2015 21:49:40 +0900 Subject: [PATCH 259/408] [zsh-completion] Remember what ^I was originally bound to (#230) --- shell/completion.zsh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 9035011..0792a09 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -104,7 +104,7 @@ fzf-completion() { # http://zsh.sourceforge.net/FAQ/zshfaq03.html tokens=(${=LBUFFER}) if [ ${#tokens} -lt 1 ]; then - zle expand-or-complete + eval "zle ${fzf_default_completion:-expand-or-complete}" return fi @@ -145,10 +145,12 @@ fzf-completion() { fi # Fall back to default completion else - zle expand-or-complete + eval "zle ${fzf_default_completion:-expand-or-complete}" fi } +fzf_default_completion=$(bindkey '^I' | grep -v undefined-key | awk '{print $2}') + zle -N fzf-completion bindkey '^I' fzf-completion From 2b61dc6557b3ecdca7f8641b0c7c4d55e03d725b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 11 May 2015 22:53:35 +0900 Subject: [PATCH 260/408] [zsh-completion] Do not overwrite $fzf_default_completion --- shell/completion.zsh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 0792a09..299826c 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -149,7 +149,8 @@ fzf-completion() { fi } -fzf_default_completion=$(bindkey '^I' | grep -v undefined-key | awk '{print $2}') +[ -z "$fzf_default_completion" ] && + fzf_default_completion=$(bindkey '^I' | grep -v undefined-key | awk '{print $2}') zle -N fzf-completion bindkey '^I' fzf-completion From 26d2af5ee8b254b5a150728aac428762a6f6e92b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 12 May 2015 01:40:44 +0900 Subject: [PATCH 261/408] [zsh-completion] Respect backslash-escaped spaces (#230) --- shell/completion.zsh | 5 +++-- test/test_go.rb | 21 +++++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 299826c..99cc112 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -12,7 +12,7 @@ _fzf_path_completion() { local base lbuf find_opts fzf_opts suffix tail fzf dir leftover matches nnm - base=$1 + base=${(Q)1} lbuf=$2 find_opts=$3 fzf_opts=$4 @@ -102,7 +102,8 @@ fzf-completion() { local tokens cmd prefix trigger tail fzf matches lbuf d_cmds # http://zsh.sourceforge.net/FAQ/zshfaq03.html - tokens=(${=LBUFFER}) + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags + tokens=(${(z)LBUFFER}) if [ ${#tokens} -lt 1 ]; then eval "zle ${fzf_default_completion:-expand-or-complete}" return diff --git a/test/test_go.rb b/test/test_go.rb index b6b37dc..b6be9e0 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -582,7 +582,12 @@ end module CompletionTest def test_file_completion - tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}; touch ~/fzf-home no~such~user', :Enter + FileUtils.mkdir_p '/tmp/fzf-test' + FileUtils.mkdir_p '/tmp/fzf test' + (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/#{i}" } + ['no~such~user', '/tmp/fzf test/foobar', '~/fzf-home'].each do |f| + FileUtils.touch File.expand_path(f) + end tmux.prepare tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 tmux.until(1) { |lines| lines.item_count > 0 } @@ -614,8 +619,20 @@ module CompletionTest tmux.send_keys 'C-L' lines[-1].end_with?('no~such~user') end + + # /tmp/fzf\ test** + tmux.send_keys 'C-u' + tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab, pane: 0 + tmux.until(1) { |lines| lines.item_count > 0 } + tmux.send_keys :Enter + tmux.until do |lines| + tmux.send_keys 'C-L' + lines[-1].end_with?('/tmp/fzf\ test/foobar') + end ensure - File.unlink 'no~such~user' + ['/tmp/fzf-test', '/tmp/fzf test', '~/fzf-home', 'no~such~user'].each do |f| + FileUtils.rm_rf File.expand_path(f) + end end def test_dir_completion From 3b91467941d2a7f00181070e65a36ebdc30a65ee Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 12 May 2015 22:54:48 +0900 Subject: [PATCH 262/408] Suppress error message when loading completion.{zsh,bash} Temporary workaround for https://github.com/Homebrew/homebrew/issues/39669 --- install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install b/install index efe9b6a..ab4cb21 100755 --- a/install +++ b/install @@ -176,7 +176,7 @@ for shell in bash zsh; do echo -n "Generate ~/.fzf.$shell ... " src=~/.fzf.${shell} - fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\"" + fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null" if [ $auto_completion -ne 0 ]; then fzf_completion="# $fzf_completion" fi From b4a05ff27ed8b1371dc3745ce02f48430345761b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 13 May 2015 19:13:27 +0900 Subject: [PATCH 263/408] [bash] CTRL-R to use history-expand-line Close #146 --- shell/key-bindings.bash | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 362e442..aad7027 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -34,7 +34,11 @@ __fzf_cd__() { } __fzf_history__() { - HISTTIMEFORMAT= history | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed "s/ *[0-9]* *//" + local line + line=$( + HISTTIMEFORMAT= history | + $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | + \grep '^ *[0-9]') && sed 's/ *\([0-9]*\) .*/!\1/' <<< "$line" } __use_tmux=0 @@ -43,6 +47,7 @@ __use_tmux=0 if [ -z "$(set -o | \grep '^vi.*on')" ]; then # Required to refresh the prompt after fzf bind '"\er": redraw-current-line' + bind '"\e^": history-expand-line' # CTRL-T - Paste the selected file path into the command line if [ $__use_tmux -eq 1 ]; then @@ -52,13 +57,14 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then fi # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": " \C-e\C-u$(__fzf_history__)\e\C-e\er"' + bind '"\C-r": " \C-e\C-u$(__fzf_history__)\e\C-e\e^\er"' # ALT-C - cd into the selected directory bind '"\ec": " \C-e\C-u$(__fzf_cd__)\e\C-e\er\C-m"' else bind '"\C-x\C-e": shell-expand-line' bind '"\C-x\C-r": redraw-current-line' + bind '"\C-x^": history-expand-line' # CTRL-T - Paste the selected file path into the command line # - FIXME: Selected items are attached to the end regardless of cursor position @@ -70,7 +76,7 @@ else bind -m vi-command '"\C-t": "i\C-t"' # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": "\eddi$(__fzf_history__)\C-x\C-e\e$a\C-x\C-r"' + bind '"\C-r": "\eddi$(__fzf_history__)\C-x\C-e\C-x^\e$a\C-x\C-r"' bind -m vi-command '"\C-r": "i\C-r"' # ALT-C - cd into the selected directory From f41de932d602b60a4ea31304601af6b7705581a3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 13 May 2015 23:14:03 +0900 Subject: [PATCH 264/408] [vim] Refocus MacVim window --- plugin/fzf.vim | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 68c9c9e..d2da893 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -193,9 +193,11 @@ function! s:popd(dict) endfunction function! s:xterm_launcher() - return printf('xterm -T [fzf]' - \ .' -bg "\%s" -fg "\%s"' - \ .' -geometry %dx%d+%d+%d -e bash -ic %%s', + let fmt = 'xterm -T "[fzf]" -bg "\%s" -fg "\%s" -geometry %dx%d+%d+%d -e bash -ic %%s' + if has('gui_macvim') + let fmt .= '; osascript -e "tell application \"MacVim\" to activate"' + endif + return printf(fmt, \ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"), \ &columns, &lines/2, getwinposx(), getwinposy()) endfunction From 7f59b42b051bc6d1d9ff9170d90a2a7983b05118 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 13 May 2015 23:20:10 +0900 Subject: [PATCH 265/408] [vim] Escape % # \ --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index d2da893..3f218b8 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -76,7 +76,7 @@ function! s:shellesc(arg) endfunction function! s:escape(path) - return substitute(a:path, ' ', '\\ ', 'g') + return escape(a:path, ' %#\') endfunction " Upgrade legacy options From f766531e7464f7c5174c881eb51e41524c972eef Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 May 2015 02:12:52 +0900 Subject: [PATCH 266/408] [neovim] Make sure that fzf buffer is closed (#225) - bd! leaves the window open when there's no other listed buffer - redraw! seems to help avoid Neovim issues. --- plugin/fzf.vim | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 3f218b8..b6df614 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -283,12 +283,17 @@ function! s:execute_term(dict, command, temps) let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps } function! fzf.on_exit(id, code) let tab = tabpagenr() + let wnr = winnr() execute 'bd!' self.buf + if winnr() == wnr + close + endif if s:ptab == tab wincmd p endif call s:pushd(self.dict) try + redraw! call s:callback(self.dict, self.temps) finally call s:popd(self.dict) From 2b7c3df66ba7cd9358a97001e27c012d9899444b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 May 2015 02:19:40 +0900 Subject: [PATCH 267/408] [neovim] Check tabpagenr() as well --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index b6df614..ae26646 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -285,7 +285,7 @@ function! s:execute_term(dict, command, temps) let tab = tabpagenr() let wnr = winnr() execute 'bd!' self.buf - if winnr() == wnr + if winnr() == wnr && tabpagenr() == tab close endif if s:ptab == tab From 34e5e2dd8200fc2cf4b298cd2d5583f2875c7a70 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 May 2015 13:29:50 +0900 Subject: [PATCH 268/408] [vim] Use close+bufhidden=wipe instead of bd --- plugin/fzf.vim | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index ae26646..89794d9 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -272,7 +272,7 @@ function! s:split(dict) tabnew endif finally - setlocal winfixwidth winfixheight + setlocal winfixwidth winfixheight buftype=nofile bufhidden=wipe nobuflisted endtry endfunction @@ -283,10 +283,14 @@ function! s:execute_term(dict, command, temps) let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps } function! fzf.on_exit(id, code) let tab = tabpagenr() - let wnr = winnr() - execute 'bd!' self.buf - if winnr() == wnr && tabpagenr() == tab + if bufnr('') == self.buf + " We use close instead of bd! since Vim does not close the split when + " there's no other listed buffer close + " FIXME This should be unnecessary due to `bufhidden=wipe` but in some + " cases Neovim fails to clean up the buffer and `bufexists('[FZF]') + " returns 1 even when it cannot be seen anywhere else. e.g. `FZF!` + silent! execute 'bd!' self.buf endif if s:ptab == tab wincmd p From cca543d0cdbb99ae5649fdf9d94ea1c62c67ad2e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 20 May 2015 16:18:30 +0900 Subject: [PATCH 269/408] [zsh-completion] Fix #236 - zle redisplay --- shell/completion.zsh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 99cc112..57c4222 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -37,8 +37,8 @@ _fzf_path_completion() { matches=${matches% } if [ -n "$matches" ]; then LBUFFER="$lbuf$matches$tail" - zle redisplay fi + zle redisplay break fi dir=$(dirname "$dir") @@ -70,8 +70,8 @@ _fzf_list_completion() { matches=$(eval "$src" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$prefix") if [ -n "$matches" ]; then LBUFFER="$lbuf$matches " - zle redisplay fi + zle redisplay } _fzf_telnet_completion() { @@ -122,8 +122,8 @@ fzf-completion() { matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$LBUFFER$matches" - zle redisplay fi + zle redisplay # Trigger sequence given elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then d_cmds=(cd pushd rmdir) From 40761b11b1d535c5de1b18f6c4e2c26932a990b3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 20 May 2015 19:45:05 +0900 Subject: [PATCH 270/408] [bash] Ignore asterisk (modified) in history --- shell/key-bindings.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index aad7027..d543428 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -38,7 +38,7 @@ __fzf_history__() { line=$( HISTTIMEFORMAT= history | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | - \grep '^ *[0-9]') && sed 's/ *\([0-9]*\) .*/!\1/' <<< "$line" + \grep '^ *[0-9]') && sed 's/ *\([0-9]*\)\** .*/!\1/' <<< "$line" } __use_tmux=0 From c511b45ff652ebee4a9b0b48e65b05558c7474c8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 20 May 2015 19:47:48 +0900 Subject: [PATCH 271/408] Minor tweak in test case It may take long for find command to spot the temporary file created on the home directory --- test/test_go.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index b6be9e0..36ef685 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -585,7 +585,7 @@ module CompletionTest FileUtils.mkdir_p '/tmp/fzf-test' FileUtils.mkdir_p '/tmp/fzf test' (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/#{i}" } - ['no~such~user', '/tmp/fzf test/foobar', '~/fzf-home'].each do |f| + ['no~such~user', '/tmp/fzf test/foobar', '~/.fzf-home'].each do |f| FileUtils.touch File.expand_path(f) end tmux.prepare @@ -602,12 +602,12 @@ module CompletionTest tmux.send_keys 'C-u' tmux.send_keys "cat ~#{ENV['USER']}**", :Tab, pane: 0 tmux.until(1) { |lines| lines.item_count > 0 } - tmux.send_keys 'fzf-home' - tmux.until(1) { |lines| lines[-3].end_with? 'fzf-home' } + tmux.send_keys '.fzf-home' + tmux.until(1) { |lines| lines[-3].end_with? '.fzf-home' } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' - lines[-1].end_with?('fzf-home') + lines[-1].end_with?('.fzf-home') end # ~INVALID_USERNAME** @@ -630,7 +630,7 @@ module CompletionTest lines[-1].end_with?('/tmp/fzf\ test/foobar') end ensure - ['/tmp/fzf-test', '/tmp/fzf test', '~/fzf-home', 'no~such~user'].each do |f| + ['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f| FileUtils.rm_rf File.expand_path(f) end end From 15099eb13b075891df9cb20e65bdf9ebd3acbb92 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 20 May 2015 20:42:45 +0900 Subject: [PATCH 272/408] Remove duplicate processing of command-line options --- src/core.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core.go b/src/core.go index d7a6a0b..61cabf9 100644 --- a/src/core.go +++ b/src/core.go @@ -47,10 +47,9 @@ Matcher -> EvtSearchFin -> Terminal (update list) */ // Run starts fzf -func Run(options *Options) { +func Run(opts *Options) { initProcs() - opts := ParseOptions() sort := opts.Sort > 0 rankTiebreak = opts.Tiebreak From 1592bedbe82e2c58dcc9b990655c65a1116d1ff7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 20 May 2015 21:25:15 +0900 Subject: [PATCH 273/408] Custom key binding support (#238) --- man/man1/fzf.1 | 40 +++++++++++ src/curses/curses.go | 34 +++++---- src/options.go | 91 ++++++++++++++++++++++-- src/options_test.go | 26 +++++++ src/terminal.go | 163 +++++++++++++++++++++++++++++++++---------- test/test_go.rb | 6 ++ 6 files changed, 306 insertions(+), 54 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 091adec..47f2cdf 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -154,6 +154,46 @@ e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR Key to toggle sort (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, or any single character) .TP +.BI "--bind=" "KEYBINDS" +Comma-separated list of custom key bindings. Each key binding expression +follows the following format: \fBKEY:ACTION\fR +.RS +e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR +.RE + +.RS +.B KEY: + \fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, or any single character +.RE + +.RS +.B ACTION: + abort + accept + backward-char + backward-delete-char + backward-kill-word + backward-word + beginning-of-line + clear-screen + delete-char + down + end-of-line + forward-char + forward-word + kill-line + kill-word + page-down + page-up + toggle-down + toggle-sort + toggle-up + unix-line-discard + unix-word-rubout + up + yank +.RE +.TP .B "--sync" Synchronous search for multi-staged filtering. If specified, fzf will launch ncurses finder only after the input stream is complete. diff --git a/src/curses/curses.go b/src/curses/curses.go index f326361..44fab4f 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -61,6 +61,16 @@ const ( PgUp PgDn + Up + Down + Left + Right + Home + End + + SLeft + SRight + F1 F2 F3 @@ -356,19 +366,19 @@ func escSequence(sz *int) Event { *sz = 3 switch _buf[2] { case 68: - return Event{CtrlB, 0, nil} + return Event{Left, 0, nil} case 67: - return Event{CtrlF, 0, nil} + return Event{Right, 0, nil} case 66: - return Event{CtrlJ, 0, nil} + return Event{Down, 0, nil} case 65: - return Event{CtrlK, 0, nil} + return Event{Up, 0, nil} case 90: return Event{BTab, 0, nil} case 72: - return Event{CtrlA, 0, nil} + return Event{Home, 0, nil} case 70: - return Event{CtrlE, 0, nil} + return Event{End, 0, nil} case 77: return mouseSequence(sz) case 80: @@ -390,7 +400,7 @@ func escSequence(sz *int) Event { case 51: return Event{Del, 0, nil} case 52: - return Event{CtrlE, 0, nil} + return Event{End, 0, nil} case 53: return Event{PgUp, 0, nil} case 54: @@ -398,7 +408,7 @@ func escSequence(sz *int) Event { case 49: switch _buf[3] { case 126: - return Event{CtrlA, 0, nil} + return Event{Home, 0, nil} case 59: if len(_buf) != 6 { return Event{Invalid, 0, nil} @@ -408,16 +418,16 @@ func escSequence(sz *int) Event { case 50: switch _buf[5] { case 68: - return Event{CtrlA, 0, nil} + return Event{Home, 0, nil} case 67: - return Event{CtrlE, 0, nil} + return Event{End, 0, nil} } case 53: switch _buf[5] { case 68: - return Event{AltB, 0, nil} + return Event{SLeft, 0, nil} case 67: - return Event{AltF, 0, nil} + return Event{SRight, 0, nil} } } // _buf[4] } // _buf[3] diff --git a/src/options.go b/src/options.go index 723c261..abae6f3 100644 --- a/src/options.go +++ b/src/options.go @@ -51,6 +51,7 @@ const usage = `usage: fzf [options] --print-query Print query as the first line --expect=KEYS Comma-separated list of keys to complete fzf --toggle-sort=KEY Key to toggle sort + --bind=KEYBINDS Custom key bindings. Refer to the man page. --sync Synchronous search for multi-staged filtering Environment variables @@ -112,8 +113,9 @@ type Options struct { Select1 bool Exit0 bool Filter *string - ToggleSort int + ToggleSort bool Expect []int + Keymap map[int]actionType PrintQuery bool Sync bool Version bool @@ -149,8 +151,9 @@ func defaultOptions() *Options { Select1: false, Exit0: false, Filter: nil, - ToggleSort: 0, + ToggleSort: false, Expect: []int{}, + Keymap: defaultKeymap(), PrintQuery: false, Sync: false, Version: false} @@ -290,12 +293,82 @@ func parseTheme(str string) *curses.ColorTheme { return nil } -func checkToggleSort(str string) int { +func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) { + for _, pairStr := range strings.Split(str, ",") { + pair := strings.Split(pairStr, ":") + if len(pair) != 2 { + errorExit("invalid key binding: " + pairStr) + } + keys := parseKeyChords(pair[0], "key name required") + if len(keys) != 1 { + errorExit("invalid key binding: " + pairStr) + } + key := keys[0] + act := strings.ToLower(pair[1]) + switch strings.ToLower(pair[1]) { + case "beginning-of-line": + keymap[key] = actBeginningOfLine + case "abort": + keymap[key] = actAbort + case "accept": + keymap[key] = actAccept + case "backward-char": + keymap[key] = actBackwardChar + case "backward-delete-char": + keymap[key] = actBackwardDeleteChar + case "backward-word": + keymap[key] = actBackwardWord + case "clear-screen": + keymap[key] = actClearScreen + case "delete-char": + keymap[key] = actDeleteChar + case "end-of-line": + keymap[key] = actEndOfLine + case "forward-char": + keymap[key] = actForwardChar + case "forward-word": + keymap[key] = actForwardWord + case "kill-line": + keymap[key] = actKillLine + case "kill-word": + keymap[key] = actKillWord + case "unix-line-discard", "line-discard": + keymap[key] = actUnixLineDiscard + case "unix-word-rubout", "word-rubout": + keymap[key] = actUnixWordRubout + case "yank": + keymap[key] = actYank + case "backward-kill-word": + keymap[key] = actBackwardKillWord + case "toggle-down": + keymap[key] = actToggleDown + case "toggle-up": + keymap[key] = actToggleUp + case "down": + keymap[key] = actDown + case "up": + keymap[key] = actUp + case "page-up": + keymap[key] = actPageUp + case "page-down": + keymap[key] = actPageDown + case "toggle-sort": + keymap[key] = actToggleSort + toggleSort = true + default: + errorExit("unknown action: " + act) + } + } + return keymap, toggleSort +} + +func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType { keys := parseKeyChords(str, "key name required") if len(keys) != 1 { errorExit("multiple keys specified") } - return keys[0] + keymap[keys[0]] = actToggleSort + return keymap } func parseOptions(opts *Options, allArgs []string) { @@ -319,10 +392,13 @@ func parseOptions(opts *Options, allArgs []string) { opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") case "--tiebreak": opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) + case "--bind": + opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) case "--color": opts.Theme = parseTheme(nextString(allArgs, &i, "color scheme name required")) case "--toggle-sort": - opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required")) + opts.Keymap = checkToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required")) + opts.ToggleSort = true case "-d", "--delimiter": opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) case "-n", "--nth": @@ -409,13 +485,16 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, _ := optString(arg, "-s|--sort="); match { opts.Sort = 1 // Don't care } else if match, value := optString(arg, "--toggle-sort="); match { - opts.ToggleSort = checkToggleSort(value) + opts.Keymap = checkToggleSort(opts.Keymap, value) + opts.ToggleSort = true } else if match, value := optString(arg, "--expect="); match { opts.Expect = parseKeyChords(value, "key names required") } else if match, value := optString(arg, "--tiebreak="); match { opts.Tiebreak = parseTiebreak(value) } else if match, value := optString(arg, "--color="); match { opts.Theme = parseTheme(value) + } else if match, value := optString(arg, "--bind="); match { + opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, value) } else { errorExit("unknown option: " + arg) } diff --git a/src/options_test.go b/src/options_test.go index 36959da..ad9a6fb 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -129,3 +129,29 @@ func TestParseKeysWithComma(t *testing.T) { check(len(keys), 1) check(keys[0], curses.AltZ+',') } + +func TestBind(t *testing.T) { + check := func(action actionType, expected actionType) { + if action != expected { + t.Errorf("%d != %d", action, expected) + } + } + keymap := defaultKeymap() + check(actBeginningOfLine, keymap[curses.CtrlA]) + keymap, toggleSort := + parseKeymap(keymap, false, + "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down") + if !toggleSort { + t.Errorf("toggleSort not set") + } + check(actKillLine, keymap[curses.CtrlA]) + check(actToggleSort, keymap[curses.CtrlB]) + check(actPageUp, keymap[curses.AltZ+'c']) + check(actPageDown, keymap[curses.AltZ]) + + keymap, toggleSort = parseKeymap(keymap, false, "f1:abort") + if toggleSort { + t.Errorf("toggleSort set") + } + check(actAbort, keymap[curses.F1]) +} diff --git a/src/terminal.go b/src/terminal.go index c38d714..1a78fed 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -31,8 +31,9 @@ type Terminal struct { input []rune multi bool sort bool - toggleSort int + toggleSort bool expect []int + keymap map[int]actionType pressed int printQuery bool count int @@ -80,6 +81,87 @@ const ( reqQuit ) +type actionType int + +const ( + actIgnore actionType = iota + actInvalid + actRune + actMouse + actBeginningOfLine + actAbort + actAccept + actBackwardChar + actBackwardDeleteChar + actBackwardWord + actClearScreen + actDeleteChar + actEndOfLine + actForwardChar + actForwardWord + actKillLine + actKillWord + actUnixLineDiscard + actUnixWordRubout + actYank + actBackwardKillWord + actToggleDown + actToggleUp + actDown + actUp + actPageUp + actPageDown + actToggleSort +) + +func defaultKeymap() map[int]actionType { + keymap := make(map[int]actionType) + keymap[C.Invalid] = actInvalid + keymap[C.CtrlA] = actBeginningOfLine + keymap[C.CtrlB] = actBackwardChar + keymap[C.CtrlC] = actAbort + keymap[C.CtrlG] = actAbort + keymap[C.CtrlQ] = actAbort + keymap[C.ESC] = actAbort + keymap[C.CtrlD] = actDeleteChar + keymap[C.CtrlE] = actEndOfLine + keymap[C.CtrlF] = actForwardChar + keymap[C.CtrlH] = actBackwardDeleteChar + keymap[C.Tab] = actToggleDown + keymap[C.BTab] = actToggleUp + keymap[C.CtrlJ] = actDown + keymap[C.CtrlK] = actUp + keymap[C.CtrlL] = actClearScreen + keymap[C.CtrlM] = actAccept + keymap[C.CtrlN] = actDown + keymap[C.CtrlP] = actUp + keymap[C.CtrlU] = actUnixLineDiscard + keymap[C.CtrlW] = actUnixWordRubout + keymap[C.CtrlY] = actYank + + keymap[C.AltB] = actBackwardWord + keymap[C.SLeft] = actBackwardWord + keymap[C.AltF] = actForwardWord + keymap[C.SRight] = actForwardWord + keymap[C.AltD] = actKillWord + keymap[C.AltBS] = actBackwardKillWord + + keymap[C.Up] = actUp + keymap[C.Down] = actDown + keymap[C.Left] = actBackwardChar + keymap[C.Right] = actForwardChar + + keymap[C.Home] = actBeginningOfLine + keymap[C.End] = actEndOfLine + keymap[C.Del] = actDeleteChar // FIXME Del vs. CTRL-D + keymap[C.PgUp] = actPageUp + keymap[C.PgDn] = actPageDown + + keymap[C.Rune] = actRune + keymap[C.Mouse] = actMouse + return keymap +} + // NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) @@ -97,6 +179,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { sort: opts.Sort > 0, toggleSort: opts.ToggleSort, expect: opts.Expect, + keymap: opts.Keymap, pressed: 0, printQuery: opts.PrintQuery, merger: EmptyMerger, @@ -249,7 +332,7 @@ func (t *Terminal) printInfo() { } output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) - if t.toggleSort > 0 { + if t.toggleSort { if t.sort { output += "/S" } else { @@ -601,105 +684,113 @@ func (t *Terminal) Loop() { break } } - if t.toggleSort > 0 { - if keyMatch(t.toggleSort, event) { - t.sort = !t.sort - t.eventBox.Set(EvtSearchNew, t.sort) - t.mutex.Unlock() - continue + + action := t.keymap[event.Type] + if event.Type == C.Rune { + code := int(event.Char) + int(C.AltZ) + if act, prs := t.keymap[code]; prs { + action = act } } - switch event.Type { - case C.Invalid: + switch action { + case actInvalid: t.mutex.Unlock() continue - case C.CtrlA: + case actToggleSort: + t.sort = !t.sort + t.eventBox.Set(EvtSearchNew, t.sort) + t.mutex.Unlock() + continue + case actBeginningOfLine: t.cx = 0 - case C.CtrlB: + case actBackwardChar: if t.cx > 0 { t.cx-- } - case C.CtrlC, C.CtrlG, C.CtrlQ, C.ESC: + case actAbort: req(reqQuit) - case C.CtrlD: + case actDeleteChar: if !t.delChar() && t.cx == 0 { req(reqQuit) } - case C.CtrlE: + case actEndOfLine: t.cx = len(t.input) - case C.CtrlF: + case actForwardChar: if t.cx < len(t.input) { t.cx++ } - case C.CtrlH: + case actBackwardDeleteChar: if t.cx > 0 { t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) t.cx-- } - case C.Tab: + case actToggleDown: if t.multi && t.merger.Length() > 0 { toggle() t.vmove(-1) req(reqList) } - case C.BTab: + case actToggleUp: if t.multi && t.merger.Length() > 0 { toggle() t.vmove(1) req(reqList) } - case C.CtrlJ, C.CtrlN: + case actDown: t.vmove(-1) req(reqList) - case C.CtrlK, C.CtrlP: + case actUp: t.vmove(1) req(reqList) - case C.CtrlM: + case actAccept: req(reqClose) - case C.CtrlL: + case actClearScreen: req(reqRedraw) - case C.CtrlU: + case actUnixLineDiscard: if t.cx > 0 { t.yanked = copySlice(t.input[:t.cx]) t.input = t.input[t.cx:] t.cx = 0 } - case C.CtrlW: + case actUnixWordRubout: if t.cx > 0 { t.rubout("\\s\\S") } - case C.AltBS: + case actBackwardKillWord: if t.cx > 0 { t.rubout("[^[:alnum:]][[:alnum:]]") } - case C.CtrlY: + case actYank: suffix := copySlice(t.input[t.cx:]) t.input = append(append(t.input[:t.cx], t.yanked...), suffix...) t.cx += len(t.yanked) - case C.Del: - t.delChar() - case C.PgUp: + case actPageUp: t.vmove(t.maxItems() - 1) req(reqList) - case C.PgDn: + case actPageDown: t.vmove(-(t.maxItems() - 1)) req(reqList) - case C.AltB: + case actBackwardWord: t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 - case C.AltF: + case actForwardWord: t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 - case C.AltD: + case actKillWord: ncx := t.cx + findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 if ncx > t.cx { t.yanked = copySlice(t.input[t.cx:ncx]) t.input = append(t.input[:t.cx], t.input[ncx:]...) } - case C.Rune: + case actKillLine: + if t.cx < len(t.input) { + t.yanked = copySlice(t.input[t.cx:]) + t.input = t.input[:t.cx] + } + case actRune: prefix := copySlice(t.input[:t.cx]) t.input = append(append(prefix, event.Char), t.input[t.cx:]...) t.cx++ - case C.Mouse: + case actMouse: me := event.MouseEvent mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y if !t.reverse { diff --git a/test/test_go.rb b/test/test_go.rb index 36ef685..53e2bc4 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -516,6 +516,12 @@ class TestGoFZF < TestBase assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i end + def test_bind + tmux.send_keys "seq 1 1000 | #{fzf '-m --bind=ctrl-j:accept,z:up,x:toggle-up'}", :Enter + tmux.until { |lines| lines[-2].end_with? '/1000' } + tmux.send_keys 'zzz', 'xx', 'C-j' + assert_equal %w[4 5], readonce.split($/) + end private def writelines path, lines File.unlink path while File.exists? path From 34946b72a5e5a3d3c5c31fef4b373dcbc7b3821f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 21 May 2015 00:38:40 +0900 Subject: [PATCH 274/408] 0.9.12 --- CHANGELOG.md | 11 +++++++++++ fzf | 4 ++-- install | 2 +- man/man1/fzf.1 | 2 +- shell/completion.bash | 5 +++-- src/constants.go | 2 +- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be5d22d..3afe698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ CHANGELOG ========= +0.9.12 +------ + +### New features + +- Added `--bind` option for custom key bindings + +### Bug fixes + +- Fixed to update "inline-info" immediately after terminal resize + 0.9.11 ------ diff --git a/fzf b/fzf index 9e85e04..a29ae33 100755 --- a/fzf +++ b/fzf @@ -206,10 +206,10 @@ class FZF @expect = true when /^--expect=(.*)$/ @expect = true - when '--toggle-sort', '--tiebreak', '--color' + when '--toggle-sort', '--tiebreak', '--color', '--bind' argv.shift when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll', - '--inline-info', '--no-inline-info', + '--inline-info', '--no-inline-info', /^--bind=(.*)$/, /^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/ # XXX else diff --git a/install b/install index ab4cb21..96e82b4 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.11 +version=0.9.12 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 47f2cdf..a696cb7 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "April 2015" "fzf 0.9.11" "fzf - a command-line fuzzy finder" +.TH fzf 1 "May 2015" "fzf 0.9.12" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/shell/completion.bash b/shell/completion.bash index ac8198e..59bdfe4 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -29,6 +29,7 @@ _fzf_opts_completion() { +s --no-sort --tac --tiebreak + --bind -m --multi --no-mouse --color @@ -222,11 +223,11 @@ a_cmds=" x_cmds="kill ssh telnet unset unalias export" # Preserve existing completion -if [ "$_fzf_completion_loaded" != '0.8.6-1' ]; then +if [ "$_fzf_completion_loaded" != '0.9.12' ]; then # Really wish I could use associative array but OSX comes with bash 3.2 :( eval $(complete | \grep '\-F' | \grep -v _fzf_ | \grep -E " ($(echo $d_cmds $f_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) - export _fzf_completion_loaded=0.8.6-1 + export _fzf_completion_loaded=0.9.12 fi if type _completion_loader > /dev/null 2>&1; then diff --git a/src/constants.go b/src/constants.go index e3b4c86..b0710af 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - Version = "0.9.11" + Version = "0.9.12" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 801cf9ac6241dc4a01a5f4ba87fcc9746c15a20e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 21 May 2015 01:35:35 +0900 Subject: [PATCH 275/408] Add unbound "toggle" action for customization --- man/man1/fzf.1 | 5 +++-- src/options.go | 2 ++ src/terminal.go | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index a696cb7..86f07de 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -181,12 +181,13 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR end-of-line forward-char forward-word - kill-line + kill-line (not bound) kill-word page-down page-up + toggle (not bound) toggle-down - toggle-sort + toggle-sort (not bound; equivalent to \fB--toggle-sort\fR) toggle-up unix-line-discard unix-word-rubout diff --git a/src/options.go b/src/options.go index abae6f3..d0014a7 100644 --- a/src/options.go +++ b/src/options.go @@ -344,6 +344,8 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in keymap[key] = actToggleDown case "toggle-up": keymap[key] = actToggleUp + case "toggle": + keymap[key] = actToggle case "down": keymap[key] = actDown case "up": diff --git a/src/terminal.go b/src/terminal.go index 1a78fed..9099576 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -105,6 +105,7 @@ const ( actUnixWordRubout actYank actBackwardKillWord + actToggle actToggleDown actToggleUp actDown @@ -724,6 +725,11 @@ func (t *Terminal) Loop() { t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) t.cx-- } + case actToggle: + if t.multi && t.merger.Length() > 0 { + toggle() + req(reqList) + } case actToggleDown: if t.multi && t.merger.Length() > 0 { toggle() From 3cdf71801e5abebeadce43a25eac461c033e8ede Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 21 May 2015 01:51:24 +0900 Subject: [PATCH 276/408] Update --help --- man/man1/fzf.1 | 56 +++++++++++++++++++++++++------------------------- src/options.go | 4 ++-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 86f07de..1a9720b 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -121,34 +121,6 @@ Display finder info inline with the query .TP .BI "--prompt=" "STR" Input prompt (default: '> ') -.SS Scripting -.TP -.BI "-q, --query=" "STR" -Start the finder with the given query -.TP -.B "-1, --select-1" -Automatically select the only match -.TP -.B "-0, --exit-0" -Exit immediately when there's no match -.TP -.BI "-f, --filter=" "STR" -Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR, -fzf becomes a fuzzy-version of grep. -.TP -.B "--print-query" -Print query as the first line -.TP -.BI "--expect=" "KEY[,..]" -Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, -or any single character) that can be used to complete fzf in addition to the -default enter key. When this option is set, fzf will print the name of the key -pressed as the first line of its output (or as the second line if -\fB--print-query\fR is also used). The line will be empty if fzf is completed -with the default enter key. -.RS -e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR -.RE .TP .BI "--toggle-sort=" "KEY" Key to toggle sort (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, @@ -194,6 +166,34 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR up yank .RE +.SS Scripting +.TP +.BI "-q, --query=" "STR" +Start the finder with the given query +.TP +.B "-1, --select-1" +Automatically select the only match +.TP +.B "-0, --exit-0" +Exit immediately when there's no match +.TP +.BI "-f, --filter=" "STR" +Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR, +fzf becomes a fuzzy-version of grep. +.TP +.B "--print-query" +Print query as the first line +.TP +.BI "--expect=" "KEY[,..]" +Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, +or any single character) that can be used to complete fzf in addition to the +default enter key. When this option is set, fzf will print the name of the key +pressed as the first line of its output (or as the second line if +\fB--print-query\fR is also used). The line will be empty if fzf is completed +with the default enter key. +.RS +e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR +.RE .TP .B "--sync" Synchronous search for multi-staged filtering. If specified, fzf will launch diff --git a/src/options.go b/src/options.go index d0014a7..97efaa7 100644 --- a/src/options.go +++ b/src/options.go @@ -42,6 +42,8 @@ const usage = `usage: fzf [options] --no-hscroll Disable horizontal scroll --inline-info Display finder info inline with the query --prompt=STR Input prompt (default: '> ') + --toggle-sort=KEY Key to toggle sort + --bind=KEYBINDS Custom key bindings. Refer to the man page. Scripting -q, --query=STR Start the finder with the given query @@ -50,8 +52,6 @@ const usage = `usage: fzf [options] -f, --filter=STR Filter mode. Do not start interactive finder. --print-query Print query as the first line --expect=KEYS Comma-separated list of keys to complete fzf - --toggle-sort=KEY Key to toggle sort - --bind=KEYBINDS Custom key bindings. Refer to the man page. --sync Synchronous search for multi-staged filtering Environment variables From 8f99f8fcc638e97d61747e173eefc3b4f11f2443 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 21 May 2015 21:06:52 +0900 Subject: [PATCH 277/408] More test cases for --bind --- test/test_go.rb | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 53e2bc4..4c4a77c 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -439,16 +439,18 @@ class TestGoFZF < TestBase end def test_toggle_sort - tmux.send_keys "seq 1 111 | #{fzf '-m +s --tac --toggle-sort=ctrl-r -q11'}", :Enter - tmux.until { |lines| lines[-3].include? '> 111' } - tmux.send_keys :Tab - tmux.until { |lines| lines[-2].include? '4/111 (1)' } - tmux.send_keys 'C-R' - tmux.until { |lines| lines[-3].include? '> 11' } - tmux.send_keys :Tab - tmux.until { |lines| lines[-2].include? '4/111/S (2)' } - tmux.send_keys :Enter - assert_equal ['111', '11'], readonce.split($/) + ['--toggle-sort=ctrl-r', '--bind=ctrl-r:toggle-sort'].each do |opt| + tmux.send_keys "seq 1 111 | #{fzf "-m +s --tac #{opt} -q11"}", :Enter + tmux.until { |lines| lines[-3].include? '> 111' } + tmux.send_keys :Tab + tmux.until { |lines| lines[-2].include? '4/111 (1)' } + tmux.send_keys 'C-R' + tmux.until { |lines| lines[-3].include? '> 11' } + tmux.send_keys :Tab + tmux.until { |lines| lines[-2].include? '4/111/S (2)' } + tmux.send_keys :Enter + assert_equal ['111', '11'], readonce.split($/) + end end def test_unicode_case @@ -517,10 +519,11 @@ class TestGoFZF < TestBase end def test_bind - tmux.send_keys "seq 1 1000 | #{fzf '-m --bind=ctrl-j:accept,z:up,x:toggle-up'}", :Enter + tmux.send_keys "seq 1 1000 | #{ + fzf '-m --bind=ctrl-j:accept,u:up,T:toggle-up,t:toggle'}", :Enter tmux.until { |lines| lines[-2].end_with? '/1000' } - tmux.send_keys 'zzz', 'xx', 'C-j' - assert_equal %w[4 5], readonce.split($/) + tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j' + assert_equal %w[4 5 6 9], readonce.split($/) end private def writelines path, lines From 4e0e492427a6517a9311546da10a4e4b91d883e5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 22 May 2015 00:02:14 +0900 Subject: [PATCH 278/408] Minor refactoring --- src/options.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/options.go b/src/options.go index 97efaa7..1531749 100644 --- a/src/options.go +++ b/src/options.go @@ -295,17 +295,20 @@ func parseTheme(str string) *curses.ColorTheme { func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) { for _, pairStr := range strings.Split(str, ",") { + fail := func() { + errorExit("invalid key binding: " + pairStr) + } pair := strings.Split(pairStr, ":") if len(pair) != 2 { - errorExit("invalid key binding: " + pairStr) + fail() } keys := parseKeyChords(pair[0], "key name required") if len(keys) != 1 { - errorExit("invalid key binding: " + pairStr) + fail() } key := keys[0] act := strings.ToLower(pair[1]) - switch strings.ToLower(pair[1]) { + switch act { case "beginning-of-line": keymap[key] = actBeginningOfLine case "abort": From b68e59a24b0bcbe84bbea2307b363d6510da3c9c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 22 May 2015 02:20:10 +0900 Subject: [PATCH 279/408] Fix ANSI offset calculation --- src/ansi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansi.go b/src/ansi.go index 126a992..116282c 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -50,7 +50,7 @@ func extractColor(str *string) (*string, []ansiOffset) { if !newState.equals(state) { if state != nil { // Update last offset - (&offsets[len(offsets)-1]).offset[1] = int32(output.Len()) + (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes())) } if newState.colored() { From 446e8227236c4d15bb17e80db797d1799b05521b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 22 May 2015 02:37:38 +0900 Subject: [PATCH 280/408] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3afe698..9bc55d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG ### Bug fixes - Fixed to update "inline-info" immediately after terminal resize +- Fixed ANSI code offset calculation 0.9.11 ------ From fdbfe36c0b882a4e948fafd1949956341607b1e5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 31 May 2015 16:46:54 +0900 Subject: [PATCH 281/408] Color customization (#245) --- man/man1/fzf.1 | 47 ++++++++++------ src/curses/curses.go | 125 ++++++++++++++++++++++++++----------------- src/item.go | 18 +++++-- src/options.go | 111 ++++++++++++++++++++++++++++++-------- src/options_test.go | 46 ++++++++++++++++ 5 files changed, 257 insertions(+), 90 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 1a9720b..9f652e0 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -91,21 +91,38 @@ Enable processing of ANSI color codes .B "--no-mouse" Disable mouse .TP -.B "--color=COL" -Color scheme: [dark|light|16|bw] -.br -(default: dark on 256-color terminal, otherwise 16) -.br -.R "" -.br -.BR dark " Color scheme for dark 256-color terminal" -.br -.BR light " Color scheme for light 256-color terminal" -.br -.BR 16 " Color scheme for 16-color terminal" -.br -.BR bw " No colors" -.br +.BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]" +Color configuration. The name of the base color scheme is followed by custom +color mappings. Ansi color code of -1 denotes terminal default +foreground/background color. + +.RS +e.g. \fBfzf --color=bg+:24\fR + \fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR +.RE + +.RS +.B BASE SCHEME: + (default: dark on 256-color terminal, otherwise 16) + + \fBdark \fRColor scheme for dark 256-color terminal + \fBlight \fRColor scheme for light 256-color terminal + \fB16 \fRColor scheme for 16-color terminal + \fBbw \fRNo colors + +.B COLOR: + \fBfg \fRText + \fBbg \fRBackground + \fBhl \fRHighlighted substrings + \fBfg+ \fRText (current line) + \fBbg+ \fRBackground (current line) + \fBhl+ \fRHighlighted substrings (current line) + \fBinfo \fRInfo + \fBprompt \fRPrompt + \fBpointer \fRPointer to the current line + \fBmarker \fRMulti-select marker + \fBspinner \fRStreaming input indicator +.RE .TP .B "--black" Use black background diff --git a/src/curses/curses.go b/src/curses/curses.go index 44fab4f..985dd87 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -106,15 +106,18 @@ const ( ) type ColorTheme struct { - darkBg C.short - prompt C.short - match C.short - current C.short - currentMatch C.short - spinner C.short - info C.short - cursor C.short - selected C.short + UseDefault bool + Fg int16 + Bg int16 + DarkBg int16 + Prompt int16 + Match int16 + Current int16 + CurrentMatch int16 + Spinner int16 + Info int16 + Cursor int16 + Selected int16 } type Event struct { @@ -142,7 +145,10 @@ var ( Default16 *ColorTheme Dark256 *ColorTheme Light256 *ColorTheme - DarkBG C.short + FG int + CurrentFG int + BG int + DarkBG int ) func init() { @@ -150,35 +156,44 @@ func init() { _clickY = []int{} _colorMap = make(map[int]int) Default16 = &ColorTheme{ - darkBg: C.COLOR_BLACK, - prompt: C.COLOR_BLUE, - match: C.COLOR_GREEN, - current: C.COLOR_YELLOW, - currentMatch: C.COLOR_GREEN, - spinner: C.COLOR_GREEN, - info: C.COLOR_WHITE, - cursor: C.COLOR_RED, - selected: C.COLOR_MAGENTA} + UseDefault: true, + Fg: 15, + Bg: 0, + DarkBg: C.COLOR_BLACK, + Prompt: C.COLOR_BLUE, + Match: C.COLOR_GREEN, + Current: C.COLOR_YELLOW, + CurrentMatch: C.COLOR_GREEN, + Spinner: C.COLOR_GREEN, + Info: C.COLOR_WHITE, + Cursor: C.COLOR_RED, + Selected: C.COLOR_MAGENTA} Dark256 = &ColorTheme{ - darkBg: 236, - prompt: 110, - match: 108, - current: 254, - currentMatch: 151, - spinner: 148, - info: 144, - cursor: 161, - selected: 168} + UseDefault: true, + Fg: 15, + Bg: 0, + DarkBg: 236, + Prompt: 110, + Match: 108, + Current: 254, + CurrentMatch: 151, + Spinner: 148, + Info: 144, + Cursor: 161, + Selected: 168} Light256 = &ColorTheme{ - darkBg: 251, - prompt: 25, - match: 66, - current: 237, - currentMatch: 23, - spinner: 65, - info: 101, - cursor: 161, - selected: 168} + UseDefault: true, + Fg: 15, + Bg: 0, + DarkBg: 251, + Prompt: 25, + Match: 66, + Current: 237, + CurrentMatch: 23, + Spinner: 65, + Info: 101, + Cursor: 161, + Selected: 168} } func attrColored(pair int, bold bool) C.int { @@ -268,23 +283,35 @@ func Init(theme *ColorTheme, black bool, mouse bool) { } func initPairs(theme *ColorTheme, black bool) { - var bg C.short + fg := C.short(theme.Fg) + bg := C.short(theme.Bg) if black { bg = C.COLOR_BLACK - } else { - C.use_default_colors() + } else if theme.UseDefault { + fg = -1 bg = -1 + C.use_default_colors() + } + if theme.UseDefault { + FG = -1 + BG = -1 + } else { + FG = int(fg) + BG = int(bg) + C.assume_default_colors(C.int(theme.Fg), C.int(bg)) } - DarkBG = theme.darkBg - C.init_pair(ColPrompt, theme.prompt, bg) - C.init_pair(ColMatch, theme.match, bg) - C.init_pair(ColCurrent, theme.current, DarkBG) - C.init_pair(ColCurrentMatch, theme.currentMatch, DarkBG) - C.init_pair(ColSpinner, theme.spinner, bg) - C.init_pair(ColInfo, theme.info, bg) - C.init_pair(ColCursor, theme.cursor, DarkBG) - C.init_pair(ColSelected, theme.selected, DarkBG) + CurrentFG = int(theme.Current) + DarkBG = int(theme.DarkBg) + darkBG := C.short(DarkBG) + C.init_pair(ColPrompt, C.short(theme.Prompt), bg) + C.init_pair(ColMatch, C.short(theme.Match), bg) + C.init_pair(ColCurrent, C.short(theme.Current), darkBG) + C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG) + C.init_pair(ColSpinner, C.short(theme.Spinner), bg) + C.init_pair(ColInfo, C.short(theme.Info), bg) + C.init_pair(ColCursor, C.short(theme.Cursor), darkBG) + C.init_pair(ColSelected, C.short(theme.Selected), darkBG) } func Close() { diff --git a/src/item.go b/src/item.go index 711adbe..7c2f94d 100644 --- a/src/item.go +++ b/src/item.go @@ -143,13 +143,25 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset offset: Offset{int32(start), int32(idx)}, color: color, bold: bold}) } else { ansi := item.colors[curr-1] + fg := ansi.color.fg + if fg == -1 { + if current { + fg = curses.CurrentFG + } else { + fg = curses.FG + } + } bg := ansi.color.bg - if current && bg == -1 { - bg = int(curses.DarkBG) + if bg == -1 { + if current { + bg = curses.DarkBG + } else { + bg = curses.BG + } } offsets = append(offsets, colorOffset{ offset: Offset{int32(start), int32(idx)}, - color: curses.PairFor(ansi.color.fg, bg), + color: curses.PairFor(fg, bg), bold: ansi.color.bold || bold}) } } diff --git a/src/options.go b/src/options.go index 1531749..63037ea 100644 --- a/src/options.go +++ b/src/options.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "regexp" + "strconv" "strings" "unicode/utf8" @@ -35,8 +36,7 @@ const usage = `usage: fzf [options] -m, --multi Enable multi-select with tab/shift-tab --ansi Enable processing of ANSI color codes --no-mouse Disable mouse - --color=COL Color scheme; [dark|light|16|bw] - (default: dark on 256-color terminal, otherwise 16) + --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --black Use black background --reverse Reverse orientation --no-hscroll Disable horizontal scroll @@ -121,14 +121,14 @@ type Options struct { Version bool } -func defaultOptions() *Options { - var defaultTheme *curses.ColorTheme +func defaultTheme() *curses.ColorTheme { if strings.Contains(os.Getenv("TERM"), "256") { - defaultTheme = curses.Dark256 - } else { - defaultTheme = curses.Default16 + return curses.Dark256 } + return curses.Default16 +} +func defaultOptions() *Options { return &Options{ Mode: ModeFuzzy, Case: CaseSmart, @@ -141,7 +141,7 @@ func defaultOptions() *Options { Multi: false, Ansi: false, Mouse: true, - Theme: defaultTheme, + Theme: defaultTheme(), Black: false, Reverse: false, Hscroll: true, @@ -187,6 +187,14 @@ func nextString(args []string, i *int, message string) string { return args[*i] } +func optionalNextString(args []string, i *int) string { + if len(args) > *i+1 { + *i++ + return args[*i] + } + return "" +} + func optionalNumeric(args []string, i *int) int { if len(args) > *i+1 { if strings.IndexAny(args[*i+1], "0123456789") == 0 { @@ -277,20 +285,72 @@ func parseTiebreak(str string) tiebreak { return byLength } -func parseTheme(str string) *curses.ColorTheme { - switch strings.ToLower(str) { - case "dark": - return curses.Dark256 - case "light": - return curses.Light256 - case "16": - return curses.Default16 - case "bw", "no": - return nil - default: - errorExit("invalid color scheme: " + str) +func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { + dupe := *theme + return &dupe +} + +func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme { + theme := dupeTheme(defaultTheme) + for _, str := range strings.Split(strings.ToLower(str), ",") { + switch str { + case "dark": + theme = dupeTheme(curses.Dark256) + case "light": + theme = dupeTheme(curses.Light256) + case "16": + theme = dupeTheme(curses.Default16) + case "bw", "no": + theme = nil + default: + fail := func() { + errorExit("invalid color specification: " + str) + } + // Color is disabled + if theme == nil { + errorExit("colors disabled; cannot customize colors") + } + + pair := strings.Split(str, ":") + if len(pair) != 2 { + fail() + } + ansi32, err := strconv.Atoi(pair[1]) + if err != nil || ansi32 < -1 || ansi32 > 255 { + fail() + } + ansi := int16(ansi32) + switch pair[0] { + case "fg": + theme.Fg = ansi + theme.UseDefault = theme.UseDefault && ansi < 0 + case "bg": + theme.Bg = ansi + theme.UseDefault = theme.UseDefault && ansi < 0 + case "fg+": + theme.Current = ansi + case "bg+": + theme.DarkBg = ansi + case "hl": + theme.Match = ansi + case "hl+": + theme.CurrentMatch = ansi + case "prompt": + theme.Prompt = ansi + case "spinner": + theme.Spinner = ansi + case "info": + theme.Info = ansi + case "pointer": + theme.Cursor = ansi + case "marker": + theme.Selected = ansi + default: + fail() + } + } } - return nil + return theme } func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) { @@ -400,7 +460,12 @@ func parseOptions(opts *Options, allArgs []string) { case "--bind": opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) case "--color": - opts.Theme = parseTheme(nextString(allArgs, &i, "color scheme name required")) + spec := optionalNextString(allArgs, &i) + if len(spec) == 0 { + opts.Theme = defaultTheme() + } else { + opts.Theme = parseTheme(opts.Theme, spec) + } case "--toggle-sort": opts.Keymap = checkToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required")) opts.ToggleSort = true @@ -497,7 +562,7 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--tiebreak="); match { opts.Tiebreak = parseTiebreak(value) } else if match, value := optString(arg, "--color="); match { - opts.Theme = parseTheme(value) + opts.Theme = parseTheme(opts.Theme, value) } else if match, value := optString(arg, "--bind="); match { opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, value) } else { diff --git a/src/options_test.go b/src/options_test.go index ad9a6fb..d356210 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -155,3 +155,49 @@ func TestBind(t *testing.T) { } check(actAbort, keymap[curses.F1]) } + +func TestColorSpec(t *testing.T) { + theme := curses.Dark256 + dark := parseTheme(theme, "dark") + if *dark != *theme { + t.Errorf("colors should be equivalent") + } + if dark == theme { + t.Errorf("point should not be equivalent") + } + + light := parseTheme(theme, "dark,light") + if *light == *theme { + t.Errorf("should not be equivalent") + } + if *light != *curses.Light256 { + t.Errorf("colors should be equivalent") + } + if light == theme { + t.Errorf("point should not be equivalent") + } + + customized := parseTheme(theme, "fg:231,bg:232") + if customized.Fg != 231 || customized.Bg != 232 { + t.Errorf("color not customized") + } + if *curses.Dark256 == *customized { + t.Errorf("colors should not be equivalent") + } + customized.Fg = curses.Dark256.Fg + customized.Bg = curses.Dark256.Bg + if *curses.Dark256 == *customized { + t.Errorf("colors should now be equivalent") + } + + customized = parseTheme(theme, "fg:231,dark,bg:232") + if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg { + t.Errorf("color not customized") + } + if customized.UseDefault { + t.Errorf("not using default colors") + } + if !curses.Dark256.UseDefault { + t.Errorf("using default colors") + } +} From b00bcf506e0554b918e7aa75842277bc9a65dda8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 3 Jun 2015 01:48:02 +0900 Subject: [PATCH 282/408] Fix #248 - Premature termination of Reader on long input --- src/reader.go | 26 +++++++++++++++++++++++--- test/test_go.rb | 11 +++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/reader.go b/src/reader.go index d476411..7496b77 100644 --- a/src/reader.go +++ b/src/reader.go @@ -30,9 +30,29 @@ func (r *Reader) ReadSource() { } func (r *Reader) feed(src io.Reader) { - if scanner := bufio.NewScanner(src); scanner != nil { - for scanner.Scan() { - r.pusher(scanner.Text()) + reader := bufio.NewReader(src) + eof := false +Loop: + for !eof { + buf := []byte{} + iter := 0 // TODO: max size? + for { + // "ReadLine either returns a non-nil line or it returns an error, never both" + line, isPrefix, err := reader.ReadLine() + eof = err == io.EOF + if eof { + break + } else if err != nil { + break Loop + } + iter++ + buf = append(buf, line...) + if !isPrefix { + break + } + } + if iter > 0 { + r.pusher(string(buf)) r.eventBox.Set(EvtReadNew, nil) } } diff --git a/test/test_go.rb b/test/test_go.rb index 4c4a77c..35caea7 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -525,6 +525,17 @@ class TestGoFZF < TestBase tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j' assert_equal %w[4 5 6 9], readonce.split($/) end + + def test_long_line + tempname = TEMPNAME + Time.now.to_f.to_s + data = '.' * 256 * 1024 + File.open(tempname, 'w') do |f| + f << data + end + assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp + ensure + File.unlink tempname + end private def writelines path, lines File.unlink path while File.exists? path From 52771a6226188ec20f2c1bdc481e5c15ac4d119a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 3 Jun 2015 02:09:07 +0900 Subject: [PATCH 283/408] 0.9.13 --- CHANGELOG.md | 12 ++++++++++++ install | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bc55d5..276ee19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ CHANGELOG ========= +0.9.13 +------ + +### New features + +- Color customization with the extended `--color` option + +### Bug fixes + +- Fixed premature termination of Reader in the presence of a long line which + is longer than 64KB + 0.9.12 ------ diff --git a/install b/install index 96e82b4..1b8d21b 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.12 +version=0.9.13 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 9f652e0..733d767 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "May 2015" "fzf 0.9.12" "fzf - a command-line fuzzy finder" +.TH fzf 1 "June 2015" "fzf 0.9.13" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index b0710af..20f4bf8 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - Version = "0.9.12" + Version = "0.9.13" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 98f62b191a29f21bdd49e32d1c1459d1f96c57b0 Mon Sep 17 00:00:00 2001 From: Dominik Honnef Date: Sat, 6 Jun 2015 23:25:03 +0200 Subject: [PATCH 284/408] Use ncurses's newterm instead of swapping stdout and stderr --- src/curses/curses.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index 985dd87..fc0fef2 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -4,11 +4,6 @@ package curses #include #include #cgo LDFLAGS: -lncurses -void swapOutput() { - FILE* temp = stdout; - stdout = stderr; - stderr = temp; -} */ import "C" @@ -142,6 +137,7 @@ var ( _colorMap map[int]int _prevDownTime time.Time _clickY []int + _screen *C.SCREEN Default16 *ColorTheme Dark256 *ColorTheme Light256 *ColorTheme @@ -254,10 +250,9 @@ func Init(theme *ColorTheme, black bool, mouse bool) { // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) } - C.swapOutput() - C.setlocale(C.LC_ALL, C.CString("")) - C.initscr() + _screen = C.newterm(nil, C.stderr, C.stdin) + C.set_term(_screen) if mouse { C.mousemask(C.ALL_MOUSE_EVENTS, nil) } @@ -316,7 +311,7 @@ func initPairs(theme *ColorTheme, black bool) { func Close() { C.endwin() - C.swapOutput() + C.delscreen(_screen) } func GetBytes() []byte { From 443a80f254c8a21c17de910533bb11fcd253eb02 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 7 Jun 2015 23:32:07 +0900 Subject: [PATCH 285/408] Always use the same color for multi-select markers --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 9099576..3dc0a75 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -373,7 +373,7 @@ func (t *Terminal) printItem(item *Item, current bool) { if current { C.CPrint(C.ColCursor, true, ">") if selected { - C.CPrint(C.ColCurrent, true, ">") + C.CPrint(C.ColSelected, true, ">") } else { C.CPrint(C.ColCurrent, true, " ") } From f6dd32046eabfc31671c39d98581bbc3899d6b27 Mon Sep 17 00:00:00 2001 From: Giulio Iotti Date: Mon, 8 Jun 2015 06:36:21 +0000 Subject: [PATCH 286/408] add support to nil-byte separated input strings, closes #121 --- src/core.go | 4 ++-- src/options.go | 5 +++++ src/reader.go | 36 +++++++++++++++--------------------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/core.go b/src/core.go index 61cabf9..c727708 100644 --- a/src/core.go +++ b/src/core.go @@ -113,7 +113,7 @@ func Run(opts *Options) { // Reader streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync if !streamingFilter { - reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} + reader := Reader{func(str string) { chunkList.Push(str) }, eventBox, opts.ReadZero} go reader.ReadSource() } @@ -139,7 +139,7 @@ func Run(opts *Options) { if pattern.MatchItem(item) { fmt.Println(*item.text) } - }, eventBox} + }, eventBox, opts.ReadZero} reader.ReadSource() } else { eventBox.Unwatch(EvtReadNew) diff --git a/src/options.go b/src/options.go index 63037ea..70d81cd 100644 --- a/src/options.go +++ b/src/options.go @@ -50,6 +50,7 @@ const usage = `usage: fzf [options] -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. + --null Read null-byte separated strings from input --print-query Print query as the first line --expect=KEYS Comma-separated list of keys to complete fzf --sync Synchronous search for multi-staged filtering @@ -117,6 +118,7 @@ type Options struct { Expect []int Keymap map[int]actionType PrintQuery bool + ReadZero bool Sync bool Version bool } @@ -155,6 +157,7 @@ func defaultOptions() *Options { Expect: []int{}, Keymap: defaultKeymap(), PrintQuery: false, + ReadZero: false, Sync: false, Version: false} } @@ -525,6 +528,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Exit0 = true case "+0", "--no-exit-0": opts.Exit0 = false + case "--null": + opts.ReadZero = true case "--print-query": opts.PrintQuery = true case "--no-print-query": diff --git a/src/reader.go b/src/reader.go index 7496b77..356c2db 100644 --- a/src/reader.go +++ b/src/reader.go @@ -13,6 +13,7 @@ import ( type Reader struct { pusher func(string) eventBox *util.EventBox + delimNil bool } // ReadSource reads data from the default command or from standard input @@ -30,31 +31,24 @@ func (r *Reader) ReadSource() { } func (r *Reader) feed(src io.Reader) { + delim := byte('\n') + if r.delimNil { + delim = '\000' + } reader := bufio.NewReader(src) - eof := false -Loop: - for !eof { - buf := []byte{} - iter := 0 // TODO: max size? - for { - // "ReadLine either returns a non-nil line or it returns an error, never both" - line, isPrefix, err := reader.ReadLine() - eof = err == io.EOF - if eof { - break - } else if err != nil { - break Loop + for { + line, err := reader.ReadString(delim) + if line != "" { + // "ReadString returns err != nil if and only if the returned data does not end in delim." + if err == nil { + line = line[:len(line)-1] } - iter++ - buf = append(buf, line...) - if !isPrefix { - break - } - } - if iter > 0 { - r.pusher(string(buf)) + r.pusher(line) r.eventBox.Set(EvtReadNew, nil) } + if err != nil { + break + } } } From 0be4cead2011440b0cc40f404953f1903673adc3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 8 Jun 2015 23:16:31 +0900 Subject: [PATCH 287/408] Allow ^EqualMatch$ --- src/algo/algo.go | 15 +++++++++++++++ src/pattern.go | 11 +++++++++-- src/pattern_test.go | 23 ++++++++++++++++++++--- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 3d5edb0..c93563a 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -1,6 +1,7 @@ package algo import ( + "strings" "unicode" "github.com/junegunn/fzf/src/util" @@ -159,3 +160,17 @@ func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) { } return trimmedLen - len(pattern), trimmedLen } + +func EqualMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { + if len(*runes) != len(pattern) { + return -1, -1 + } + runesStr := string(*runes) + if !caseSensitive { + runesStr = strings.ToLower(runesStr) + } + if runesStr == string(pattern) { + return 0, len(pattern) + } + return -1, -1 +} diff --git a/src/pattern.go b/src/pattern.go index 64e0c6e..ffdf6d8 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -24,6 +24,7 @@ const ( termExact termPrefix termSuffix + termEqual ) type term struct { @@ -116,6 +117,7 @@ func BuildPattern(mode Mode, caseMode Case, procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))} ptr.procFun[termFuzzy] = algo.FuzzyMatch + ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termExact] = algo.ExactMatchNaive ptr.procFun[termPrefix] = algo.PrefixMatch ptr.procFun[termSuffix] = algo.SuffixMatch @@ -151,8 +153,13 @@ func parseTerms(mode Mode, caseMode Case, str string) []term { text = text[1:] } } else if strings.HasPrefix(text, "^") { - typ = termPrefix - text = text[1:] + if strings.HasSuffix(text, "$") { + typ = termEqual + text = text[1 : len(text)-1] + } else { + typ = termPrefix + text = text[1:] + } } else if strings.HasSuffix(text, "$") { typ = termSuffix text = text[:len(text)-1] diff --git a/src/pattern_test.go b/src/pattern_test.go index 7f00272..fe6561c 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -8,8 +8,8 @@ import ( func TestParseTermsExtended(t *testing.T) { terms := parseTerms(ModeExtended, CaseSmart, - "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") - if len(terms) != 8 || + "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$") + if len(terms) != 9 || terms[0].typ != termFuzzy || terms[0].inv || terms[1].typ != termExact || terms[1].inv || terms[2].typ != termPrefix || terms[2].inv || @@ -17,7 +17,8 @@ func TestParseTermsExtended(t *testing.T) { terms[4].typ != termFuzzy || !terms[4].inv || terms[5].typ != termExact || !terms[5].inv || terms[6].typ != termPrefix || !terms[6].inv || - terms[7].typ != termSuffix || !terms[7].inv { + terms[7].typ != termSuffix || !terms[7].inv || + terms[8].typ != termEqual || terms[8].inv { t.Errorf("%s", terms) } for idx, term := range terms { @@ -65,6 +66,22 @@ func TestExact(t *testing.T) { } } +func TestEqual(t *testing.T) { + defer clearPatternCache() + clearPatternCache() + pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("^AbC$")) + + match := func(str string, sidxExpected int, eidxExpected int) { + runes := []rune(str) + sidx, eidx := algo.EqualMatch(pattern.caseSensitive, &runes, pattern.terms[0].text) + if sidx != sidxExpected || eidx != eidxExpected { + t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) + } + } + match("ABC", -1, -1) + match("AbC", 0, 3) +} + func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() From 45a3655eafa5ebd2de133e3d3bc39263c32d306b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 8 Jun 2015 23:27:50 +0900 Subject: [PATCH 288/408] Add test case for --null option --- test/test_go.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/test_go.rb b/test/test_go.rb index 35caea7..160699c 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -536,6 +536,13 @@ class TestGoFZF < TestBase ensure File.unlink tempname end + + def test_null + lines = `find .`.split($/) + assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp + assert_equal lines.last, `find . -print0 | #{FZF} --null -e -f "^#{lines.last}$"`.chomp + end + private def writelines path, lines File.unlink path while File.exists? path From 1b9b1d15bc52508a5d8421823f31150a8f77d248 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 8 Jun 2015 23:28:41 +0900 Subject: [PATCH 289/408] Adjust --help output --- src/options.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/options.go b/src/options.go index 70d81cd..edd6f9a 100644 --- a/src/options.go +++ b/src/options.go @@ -15,7 +15,7 @@ import ( const usage = `usage: fzf [options] - Search mode + Search -x, --extended Extended-search mode -e, --extended-exact Extended-search mode (exact match) -i Case-insensitive match (default: smart-case match) @@ -25,8 +25,6 @@ const usage = `usage: fzf [options] 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, --no-sort Do not sort the result --tac Reverse the order of the input --tiebreak=CRI Sort criterion when the scores are tied; From eaa3c67a5e2b1e5f23d8fd3effc38d9cee9af521 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 9 Jun 2015 23:44:54 +0900 Subject: [PATCH 290/408] Add actions for --bind: select-all / deselect-all / toggle-all Close #257 --- src/options.go | 6 +++++ src/terminal.go | 58 +++++++++++++++++++++++++++++++++++++++---------- test/test_go.rb | 23 ++++++++++++++++++++ 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/options.go b/src/options.go index edd6f9a..b4afad7 100644 --- a/src/options.go +++ b/src/options.go @@ -408,6 +408,12 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in keymap[key] = actToggleDown case "toggle-up": keymap[key] = actToggleUp + case "toggle-all": + keymap[key] = actToggleAll + case "select-all": + keymap[key] = actSelectAll + case "deselect-all": + keymap[key] = actDeselectAll case "toggle": keymap[key] = actToggle case "down": diff --git a/src/terminal.go b/src/terminal.go index 3dc0a75..d27c0d6 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -105,7 +105,10 @@ const ( actUnixWordRubout actYank actBackwardKillWord + actSelectAll + actDeselectAll actToggle + actToggleAll actToggleDown actToggleUp actDown @@ -661,20 +664,28 @@ func (t *Terminal) Loop() { } } } + selectItem := func(item *Item) bool { + if _, found := t.selected[item.index]; !found { + var strptr *string + if item.origText != nil { + strptr = item.origText + } else { + strptr = item.text + } + t.selected[item.index] = selectedItem{time.Now(), strptr} + return true + } + return false + } + toggleY := func(y int) { + item := t.merger.Get(y) + if !selectItem(item) { + delete(t.selected, item.index) + } + } toggle := func() { if t.cy < t.merger.Length() { - item := t.merger.Get(t.cy) - if _, found := t.selected[item.index]; !found { - var strptr *string - if item.origText != nil { - strptr = item.origText - } else { - strptr = item.text - } - t.selected[item.index] = selectedItem{time.Now(), strptr} - } else { - delete(t.selected, item.index) - } + toggleY(t.cy) req(reqInfo) } } @@ -725,11 +736,34 @@ func (t *Terminal) Loop() { t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) t.cx-- } + case actSelectAll: + if t.multi { + for i := 0; i < t.merger.Length(); i++ { + item := t.merger.Get(i) + selectItem(item) + } + req(reqList, reqInfo) + } + case actDeselectAll: + if t.multi { + for i := 0; i < t.merger.Length(); i++ { + item := t.merger.Get(i) + delete(t.selected, item.index) + } + req(reqList, reqInfo) + } case actToggle: if t.multi && t.merger.Length() > 0 { toggle() req(reqList) } + case actToggleAll: + if t.multi { + for i := 0; i < t.merger.Length(); i++ { + toggleY(i) + } + req(reqList, reqInfo) + } case actToggleDown: if t.multi && t.merger.Length() > 0 { toggle() diff --git a/test/test_go.rb b/test/test_go.rb index 160699c..32677ad 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -543,6 +543,29 @@ class TestGoFZF < TestBase assert_equal lines.last, `find . -print0 | #{FZF} --null -e -f "^#{lines.last}$"`.chomp end + def test_select_all_deselect_all_toggle_all + tmux.send_keys "seq 100 | #{fzf '--bind ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all --multi'}", :Enter + tmux.until { |lines| lines[-2].include? '100/100' } + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| lines[-2].include? '(3)' } + tmux.send_keys 'C-t' + tmux.until { |lines| lines[-2].include? '(97)' } + tmux.send_keys 'C-a' + tmux.until { |lines| lines[-2].include? '(100)' } + tmux.send_keys :Tab, :Tab + tmux.until { |lines| lines[-2].include? '(98)' } + tmux.send_keys 'C-d' + tmux.until { |lines| !lines[-2].include? '(' } + tmux.send_keys :Tab, :Tab + tmux.until { |lines| lines[-2].include? '(2)' } + tmux.send_keys 0 + tmux.until { |lines| lines[-2].include? '10/100' } + tmux.send_keys 'C-a' + tmux.until { |lines| lines[-2].include? '(12)' } + tmux.send_keys :Enter + assert_equal %w[2 1 10 20 30 40 50 60 70 80 90 100], readonce.split($/) + end + private def writelines path, lines File.unlink path while File.exists? path From 9f33068ab3d918651592c24f3fb1f9fddbaf226a Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Sat, 13 Jun 2015 10:34:12 -0300 Subject: [PATCH 291/408] Avoid conflict with other neoterm plugins. To avoid conflict with other neoterm plugins that manage terminals, prefer named terminals. --- plugin/fzf.vim | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 89794d9..48ad606 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -98,7 +98,7 @@ function! fzf#run(...) abort try let oshell = &shell set shell=sh - if has('nvim') && bufexists('[FZF]') + if has('nvim') && bufexists('term://*:FZF') echohl WarningMsg echomsg 'FZF is already running!' echohl None @@ -280,17 +280,13 @@ function! s:execute_term(dict, command, temps) call s:split(a:dict) call s:pushd(a:dict) - let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps } + let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' } function! fzf.on_exit(id, code) let tab = tabpagenr() if bufnr('') == self.buf " We use close instead of bd! since Vim does not close the split when " there's no other listed buffer close - " FIXME This should be unnecessary due to `bufhidden=wipe` but in some - " cases Neovim fails to clean up the buffer and `bufexists('[FZF]') - " returns 1 even when it cannot be seen anywhere else. e.g. `FZF!` - silent! execute 'bd!' self.buf endif if s:ptab == tab wincmd p @@ -305,7 +301,6 @@ function! s:execute_term(dict, command, temps) endfunction call termopen(a:command, fzf) - silent file [FZF] startinsert return [] endfunction From 3b5281179639811eb9a1e5fde8aeed1264f842ed Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 00:43:44 +0900 Subject: [PATCH 292/408] Add support for search history - Add `--history` option (e.g. fzf --history ~/.fzf.history) - Add `--history-max` option for limiting the size of the file (default 1000) - Add `previous-history` and `next-history` actions for `--bind` - CTRL-P and CTRL-N are automatically remapped to these actions when `--history` is used Closes #249, #251 --- fzf | 6 ++-- shell/completion.bash | 11 ++++-- src/constants.go | 3 ++ src/options.go | 81 ++++++++++++++++++++++++++++++++++++++++--- src/terminal.go | 27 +++++++++++++-- test/test_go.rb | 46 ++++++++++++++++++++++++ 6 files changed, 163 insertions(+), 11 deletions(-) diff --git a/fzf b/fzf index a29ae33..78a02fb 100755 --- a/fzf +++ b/fzf @@ -206,11 +206,11 @@ class FZF @expect = true when /^--expect=(.*)$/ @expect = true - when '--toggle-sort', '--tiebreak', '--color', '--bind' + when '--toggle-sort', '--tiebreak', '--color', '--bind', '--history', '--history-max' argv.shift when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll', - '--inline-info', '--no-inline-info', /^--bind=(.*)$/, - /^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/ + '--inline-info', '--no-inline-info', '--null', /^--bind=(.*)$/, + /^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/, /^--history(-max)?=(.*)$/ # XXX else usage 1, "illegal option: #{o}" diff --git a/shell/completion.bash b/shell/completion.bash index 59bdfe4..42d66c1 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -45,7 +45,10 @@ _fzf_opts_completion() { --print-query --expect --toggle-sort - --sync" + --sync + --null + --history + --history-max" case "${prev}" in --tiebreak) @@ -56,6 +59,10 @@ _fzf_opts_completion() { COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) ) return 0 ;; + --history) + COMPREPLY=() + return 0 + ;; esac if [[ ${cur} =~ ^-|\+ ]]; then @@ -207,7 +214,7 @@ EOF } # fzf options -complete -F _fzf_opts_completion fzf +complete -o default -F _fzf_opts_completion fzf d_cmds="cd pushd rmdir" f_cmds=" diff --git a/src/constants.go b/src/constants.go index 20f4bf8..87ba0f8 100644 --- a/src/constants.go +++ b/src/constants.go @@ -32,6 +32,9 @@ const ( // Not to cache mergers with large lists mergerCacheMax int = 100000 + + // History + defaultHistoryMax int = 1000 ) // fzf events diff --git a/src/options.go b/src/options.go index b4afad7..2a041ef 100644 --- a/src/options.go +++ b/src/options.go @@ -42,6 +42,8 @@ const usage = `usage: fzf [options] --prompt=STR Input prompt (default: '> ') --toggle-sort=KEY Key to toggle sort --bind=KEYBINDS Custom key bindings. Refer to the man page. + --history=FILE History file + --history-max=N Maximum number of history entries (default: 1000) Scripting -q, --query=STR Start the finder with the given query @@ -118,6 +120,7 @@ type Options struct { PrintQuery bool ReadZero bool Sync bool + History *History Version bool } @@ -157,6 +160,7 @@ func defaultOptions() *Options { PrintQuery: false, ReadZero: false, Sync: false, + History: nil, Version: false} } @@ -196,6 +200,23 @@ func optionalNextString(args []string, i *int) string { return "" } +func atoi(str string) int { + num, err := strconv.Atoi(str) + if err != nil { + errorExit("not a valid integer: " + str) + } + return num +} + +func nextInt(args []string, i *int, message string) int { + if len(args) > *i+1 { + *i++ + } else { + errorExit(message) + } + return atoi(args[*i]) +} + func optionalNumeric(args []string, i *int) int { if len(args) > *i+1 { if strings.IndexAny(args[*i+1], "0123456789") == 0 { @@ -424,6 +445,10 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in keymap[key] = actPageUp case "page-down": keymap[key] = actPageDown + case "previous-history": + keymap[key] = actPreviousHistory + case "next-history": + keymap[key] = actNextHistory case "toggle-sort": keymap[key] = actToggleSort toggleSort = true @@ -444,6 +469,29 @@ func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType { } func parseOptions(opts *Options, allArgs []string) { + keymap := make(map[int]actionType) + var historyMax int + if opts.History == nil { + historyMax = defaultHistoryMax + } else { + historyMax = opts.History.maxSize + } + setHistory := func(path string) { + h, e := NewHistory(path, historyMax) + if e != nil { + errorExit(e.Error()) + } + opts.History = h + } + setHistoryMax := func(max int) { + historyMax = max + if historyMax < 1 { + errorExit("history max must be a positive integer") + } + if opts.History != nil { + opts.History.maxSize = historyMax + } + } for i := 0; i < len(allArgs); i++ { arg := allArgs[i] switch arg { @@ -465,7 +513,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--tiebreak": opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": - opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) + keymap, opts.ToggleSort = parseKeymap(keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) case "--color": spec := optionalNextString(allArgs, &i) if len(spec) == 0 { @@ -474,7 +522,7 @@ func parseOptions(opts *Options, allArgs []string) { opts.Theme = parseTheme(opts.Theme, spec) } case "--toggle-sort": - opts.Keymap = checkToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required")) + keymap = checkToggleSort(keymap, nextString(allArgs, &i, "key name required")) opts.ToggleSort = true case "-d", "--delimiter": opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) @@ -546,6 +594,12 @@ func parseOptions(opts *Options, allArgs []string) { opts.Sync = false case "--async": opts.Sync = false + case "--no-history": + opts.History = nil + case "--history": + setHistory(nextString(allArgs, &i, "history file path required")) + case "--history-max": + setHistoryMax(nextInt(allArgs, &i, "history max size required")) case "--version": opts.Version = true default: @@ -564,7 +618,7 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, _ := optString(arg, "-s|--sort="); match { opts.Sort = 1 // Don't care } else if match, value := optString(arg, "--toggle-sort="); match { - opts.Keymap = checkToggleSort(opts.Keymap, value) + keymap = checkToggleSort(keymap, value) opts.ToggleSort = true } else if match, value := optString(arg, "--expect="); match { opts.Expect = parseKeyChords(value, "key names required") @@ -573,13 +627,32 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--color="); match { opts.Theme = parseTheme(opts.Theme, value) } else if match, value := optString(arg, "--bind="); match { - opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, value) + keymap, opts.ToggleSort = parseKeymap(keymap, opts.ToggleSort, value) + } else if match, value := optString(arg, "--history="); match { + setHistory(value) + } else if match, value := optString(arg, "--history-max="); match { + setHistoryMax(atoi(value)) } else { errorExit("unknown option: " + arg) } } } + // Change default actions for CTRL-N / CTRL-P when --history is used + if opts.History != nil { + if _, prs := keymap[curses.CtrlP]; !prs { + keymap[curses.CtrlP] = actPreviousHistory + } + if _, prs := keymap[curses.CtrlN]; !prs { + keymap[curses.CtrlN] = actNextHistory + } + } + + // Override default key bindings + for key, act := range keymap { + opts.Keymap[key] = act + } + // 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 { diff --git a/src/terminal.go b/src/terminal.go index d27c0d6..4ff2659 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -36,6 +36,7 @@ type Terminal struct { keymap map[int]actionType pressed int printQuery bool + history *History count int progress int reading bool @@ -116,6 +117,8 @@ const ( actPageUp actPageDown actToggleSort + actPreviousHistory + actNextHistory ) func defaultKeymap() map[int]actionType { @@ -186,6 +189,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { keymap: opts.Keymap, pressed: 0, printQuery: opts.PrintQuery, + history: opts.History, merger: EmptyMerger, selected: make(map[uint32]selectedItem), reqBox: util.NewEventBox(), @@ -610,6 +614,13 @@ func (t *Terminal) Loop() { }() } + exit := func(code int) { + if code == 0 && t.history != nil { + t.history.append(string(t.input)) + } + os.Exit(code) + } + go func() { for { t.reqBox.Wait(func(events *util.Events) { @@ -636,10 +647,10 @@ func (t *Terminal) Loop() { case reqClose: C.Close() t.output() - os.Exit(0) + exit(0) case reqQuit: C.Close() - os.Exit(1) + exit(1) } } t.placeCursor() @@ -830,6 +841,18 @@ func (t *Terminal) Loop() { prefix := copySlice(t.input[:t.cx]) t.input = append(append(prefix, event.Char), t.input[t.cx:]...) t.cx++ + case actPreviousHistory: + if t.history != nil { + t.history.override(string(t.input)) + t.input = []rune(t.history.previous()) + t.cx = len(t.input) + } + case actNextHistory: + if t.history != nil { + t.history.override(string(t.input)) + t.input = []rune(t.history.next()) + t.cx = len(t.input) + } case actMouse: me := event.MouseEvent mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y diff --git a/test/test_go.rb b/test/test_go.rb index 32677ad..f726760 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -566,6 +566,52 @@ class TestGoFZF < TestBase assert_equal %w[2 1 10 20 30 40 50 60 70 80 90 100], readonce.split($/) end + def test_history + history_file = '/tmp/fzf-test-history' + + # History with limited number of entries + File.unlink history_file rescue nil + opts = "--history=#{history_file} --history-max=4" + input = %w[00 11 22 33 44].map { |e| e + $/ } + input.each do |keys| + tmux.send_keys "seq 100 | #{fzf opts}", :Enter + tmux.until { |lines| lines[-2].include? '100/100' } + tmux.send_keys keys + tmux.until { |lines| lines[-2].include? '1/100' } + tmux.send_keys :Enter + end + assert_equal input[1..-1], File.readlines(history_file) + + # Update history entries (not changed on disk) + tmux.send_keys "seq 100 | #{fzf opts}", :Enter + tmux.until { |lines| lines[-2].include? '100/100' } + tmux.send_keys 'C-p' + tmux.until { |lines| lines[-1].end_with? '> 44' } + tmux.send_keys 'C-p' + tmux.until { |lines| lines[-1].end_with? '> 33' } + tmux.send_keys :BSpace + tmux.until { |lines| lines[-1].end_with? '> 3' } + tmux.send_keys 1 + tmux.until { |lines| lines[-1].end_with? '> 31' } + tmux.send_keys 'C-p' + tmux.until { |lines| lines[-1].end_with? '> 22' } + tmux.send_keys 'C-n' + tmux.until { |lines| lines[-1].end_with? '> 31' } + tmux.send_keys 0 + tmux.until { |lines| lines[-1].end_with? '> 310' } + tmux.send_keys :Enter + assert_equal %w[22 33 44 310].map { |e| e + $/ }, File.readlines(history_file) + + # Respect --bind option + tmux.send_keys "seq 100 | #{fzf opts + ' --bind ctrl-p:next-history,ctrl-n:previous-history'}", :Enter + tmux.until { |lines| lines[-2].include? '100/100' } + tmux.send_keys 'C-n', 'C-n', 'C-n', 'C-n', 'C-p' + tmux.until { |lines| lines[-1].end_with?('33') } + tmux.send_keys :Enter + ensure + File.unlink history_file + end + private def writelines path, lines File.unlink path while File.exists? path From 8e7164553f231ec7e76ce54693b1457872001333 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 00:53:45 +0900 Subject: [PATCH 293/408] Add missing files from the previous commit :( --- src/history.go | 94 +++++++++++++++++++++++++++++++++++++++++++++ src/history_test.go | 53 +++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 src/history.go create mode 100644 src/history_test.go diff --git a/src/history.go b/src/history.go new file mode 100644 index 0000000..66159eb --- /dev/null +++ b/src/history.go @@ -0,0 +1,94 @@ +package fzf + +import ( + "errors" + "io/ioutil" + "os" + "strings" +) + +type History struct { + path string + lines []string + modified map[int]string + maxSize int + cursor int +} + +func NewHistory(path string, maxSize int) (*History, error) { + fmtError := func(e error) error { + if os.IsPermission(e) { + return errors.New("permission denied: " + path) + } + return errors.New("invalid history file: " + e.Error()) + } + + // Read history file + data, err := ioutil.ReadFile(path) + if err != nil { + // If it doesn't exist, check if we can create a file with the name + if os.IsNotExist(err) { + data = []byte{} + if err := ioutil.WriteFile(path, data, 0600); err != nil { + return nil, fmtError(err) + } + } else { + return nil, fmtError(err) + } + } + // Split lines and limit the maximum number of lines + lines := strings.Split(strings.Trim(string(data), "\n"), "\n") + if len(lines[len(lines)-1]) > 0 { + lines = append(lines, "") + } + return &History{ + path: path, + maxSize: maxSize, + lines: lines, + modified: make(map[int]string), + cursor: len(lines) - 1}, nil +} + +func (h *History) append(line string) error { + // We don't append empty lines + if len(line) == 0 { + return nil + } + + lines := append(h.lines[:len(h.lines)-1], line) + if len(lines) > h.maxSize { + lines = lines[len(lines)-h.maxSize : len(lines)] + } + h.lines = append(lines, "") + return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600) +} + +func (h *History) override(str string) { + // You can update the history but they're not written to the file + if h.cursor == len(h.lines)-1 { + h.lines[h.cursor] = str + } else if h.cursor < len(h.lines)-1 { + h.modified[h.cursor] = str + } +} + +func (h *History) current() string { + if str, prs := h.modified[h.cursor]; prs { + return str + } + return h.lines[h.cursor] +} + +func (h *History) previous() string { + if h.cursor > 0 { + h.cursor-- + } + return h.current() +} + +func (h *History) next() string { + if h.cursor < len(h.lines)-1 { + h.cursor++ + } + return h.current() +} diff --git a/src/history_test.go b/src/history_test.go new file mode 100644 index 0000000..83e4029 --- /dev/null +++ b/src/history_test.go @@ -0,0 +1,53 @@ +package fzf + +import ( + "testing" +) + +func TestHistory(t *testing.T) { + maxHistory := 50 + + // Invalid arguments + for _, path := range []string{"/etc", "/proc", "/etc/sudoers"} { + if _, e := NewHistory(path, maxHistory); e == nil { + t.Error("Error expected for: " + path) + } + } + { // Append lines + h, _ := NewHistory("/tmp/fzf-history", maxHistory) + for i := 0; i < maxHistory+10; i++ { + h.append("foobar") + } + } + { // Read lines + h, _ := NewHistory("/tmp/fzf-history", maxHistory) + if len(h.lines) != maxHistory+1 { + t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) + } + for i := 0; i < maxHistory; i++ { + if h.lines[i] != "foobar" { + t.Error("Expected: foobar, actual: " + h.lines[i]) + } + } + } + { // Append lines + h, _ := NewHistory("/tmp/fzf-history", maxHistory) + h.append("barfoo") + h.append("") + h.append("foobarbaz") + } + { // Read lines again + h, _ := NewHistory("/tmp/fzf-history", maxHistory) + if len(h.lines) != maxHistory+1 { + t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) + } + compare := func(idx int, exp string) { + if h.lines[idx] != exp { + t.Errorf("Expected: %s, actual: %s\n", exp, h.lines[idx]) + } + } + compare(maxHistory-3, "foobar") + compare(maxHistory-2, "barfoo") + compare(maxHistory-1, "foobarbaz") + } +} From 9fca611c4a4b09ee4fe24f5ae65030380e79efd0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 01:54:56 +0900 Subject: [PATCH 294/408] Add `ignore` action for --bind --- src/options.go | 2 ++ src/terminal.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/options.go b/src/options.go index 2a041ef..d88706e 100644 --- a/src/options.go +++ b/src/options.go @@ -391,6 +391,8 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in key := keys[0] act := strings.ToLower(pair[1]) switch act { + case "ignore": + keymap[key] = actIgnore case "beginning-of-line": keymap[key] = actBeginningOfLine case "abort": diff --git a/src/terminal.go b/src/terminal.go index 4ff2659..372c197 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -716,6 +716,9 @@ func (t *Terminal) Loop() { } } switch action { + case actIgnore: + t.mutex.Unlock() + continue case actInvalid: t.mutex.Unlock() continue From 6ad1736832fc241534e537119d16eff3474dc09b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 02:11:27 +0900 Subject: [PATCH 295/408] Fix ignore action --- src/terminal.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 372c197..6f28a40 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -717,8 +717,6 @@ func (t *Terminal) Loop() { } switch action { case actIgnore: - t.mutex.Unlock() - continue case actInvalid: t.mutex.Unlock() continue From 8973207bb4e1b27c3123e2d5132001ea7d8bb67d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 02:13:02 +0900 Subject: [PATCH 296/408] Fix Travis CI build --- test/test_go.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_go.rb b/test/test_go.rb index f726760..d36bc4d 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -580,6 +580,7 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-2].include? '1/100' } tmux.send_keys :Enter end + readonce assert_equal input[1..-1], File.readlines(history_file) # Update history entries (not changed on disk) @@ -600,6 +601,7 @@ class TestGoFZF < TestBase tmux.send_keys 0 tmux.until { |lines| lines[-1].end_with? '> 310' } tmux.send_keys :Enter + readonce assert_equal %w[22 33 44 310].map { |e| e + $/ }, File.readlines(history_file) # Respect --bind option From 8b618f743914311662b09ecc549e4c9aa53540dc Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 02:44:22 +0900 Subject: [PATCH 297/408] Test refactoring --- test/test_go.rb | 38 +++++++++----------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index d36bc4d..43fb555 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -72,17 +72,6 @@ class Tmux end end - def closed? - !go("list-window -F '#I'").include?(win) - end - - def close - wait do - send_keys 'C-c', 'C-u', 'exit', :Enter - closed? - end - end - def kill go("kill-window -t #{win} 2> /dev/null") end @@ -152,21 +141,25 @@ class TestBase < Minitest::Test attr_reader :tmux + def tempname + [TEMPNAME, + caller_locations.map(&:label).find { |l| l =~ /^test_/ }].join '-' + end + def setup ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_COMMAND' - File.unlink TEMPNAME while File.exists?(TEMPNAME) end def readonce - wait { File.exists?(TEMPNAME) } - File.read(TEMPNAME) + wait { File.exists?(tempname) } + File.read(tempname) ensure - File.unlink TEMPNAME while File.exists?(TEMPNAME) + File.unlink tempname while File.exists?(tempname) end def fzf(*opts) - fzf!(*opts) + " > #{TEMPNAME}.tmp; mv #{TEMPNAME}.tmp #{TEMPNAME}" + fzf!(*opts) + " > #{tempname}.tmp; mv #{tempname}.tmp #{tempname}" end def fzf!(*opts) @@ -214,7 +207,6 @@ class TestGoFZF < TestBase assert_equal '> 391', lines[-1] tmux.send_keys :Enter - tmux.close assert_equal '1391', readonce.chomp end @@ -223,7 +215,6 @@ class TestGoFZF < TestBase tmux.until { |lines| lines.last =~ /^>/ } tmux.send_keys :Enter - tmux.close assert_equal 'hello', readonce.chomp end @@ -290,7 +281,6 @@ class TestGoFZF < TestBase # CTRL-M tmux.send_keys "C-M" tmux.until { |lines| lines.last !~ /^>/ } - tmux.close end def test_multi_order @@ -303,7 +293,6 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-2].include? '(6)' } tmux.send_keys "C-M" assert_equal %w[3 2 5 6 8 7], readonce.split($/) - tmux.close end def test_with_nth @@ -454,16 +443,12 @@ class TestGoFZF < TestBase end def test_unicode_case - tempname = TEMPNAME + Time.now.to_f.to_s writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4] assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/) assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/) - rescue - File.unlink tempname end def test_tiebreak - tempname = TEMPNAME + Time.now.to_f.to_s input = %w[ --foobar-------- -----foobar--- @@ -500,8 +485,6 @@ class TestGoFZF < TestBase ], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/) assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/) - rescue - File.unlink tempname end def test_invalid_cache @@ -527,14 +510,11 @@ class TestGoFZF < TestBase end def test_long_line - tempname = TEMPNAME + Time.now.to_f.to_s data = '.' * 256 * 1024 File.open(tempname, 'w') do |f| f << data end assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp - ensure - File.unlink tempname end def test_null From 77048f3e3bb47d8322863e7ac002fbf6f3c222bf Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 02:51:36 +0900 Subject: [PATCH 298/408] Fix Travis CI build --- test/test_go.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 43fb555..5521002 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -559,8 +559,8 @@ class TestGoFZF < TestBase tmux.send_keys keys tmux.until { |lines| lines[-2].include? '1/100' } tmux.send_keys :Enter + readonce end - readonce assert_equal input[1..-1], File.readlines(history_file) # Update history entries (not changed on disk) From 77bab51696b509f2c86a4737bbe66ec4830f26e7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 03:19:18 +0900 Subject: [PATCH 299/408] GoLint fix --- src/terminal.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 6f28a40..50d380f 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -943,7 +943,6 @@ func (t *Terminal) vset(o int) bool { func (t *Terminal) maxItems() int { if t.inlineInfo { return C.MaxY() - 1 - } else { - return C.MaxY() - 2 } + return C.MaxY() - 2 } From fe5b190a7d8612bb90de272d26470bd02dc76a64 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 11:23:07 +0900 Subject: [PATCH 300/408] Remove unnecessary regexp matches This change does have positive effect on startup time of fzf when many number of options are provided. time fzf --query=____ --filter=____ --delimiter=q --prompt=________ \ --nth=1,2,3,4 --with-nth=1,2,3,4 --toggle-sort=ctrl-r \ --expect=ctrl-x --tiebreak=index --color=light --bind=ctrl-t:accept \ --history=/tmp/xxx --history-max=1000 --help 0m0.013s -> 0m0.008s --- src/options.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/options.go b/src/options.go index d88706e..d969e51 100644 --- a/src/options.go +++ b/src/options.go @@ -174,11 +174,11 @@ func errorExit(msg string) { help(1) } -func optString(arg string, prefix string) (bool, string) { - rx, _ := regexp.Compile(fmt.Sprintf("^(?:%s)(.*)$", prefix)) - matches := rx.FindStringSubmatch(arg) - if len(matches) > 1 { - return true, matches[1] +func optString(arg string, prefixes ...string) (bool, string) { + for _, prefix := range prefixes { + if strings.HasPrefix(arg, prefix) { + return true, arg[len(prefix):] + } } return false, "" } @@ -605,19 +605,19 @@ func parseOptions(opts *Options, allArgs []string) { case "--version": opts.Version = true default: - if match, value := optString(arg, "-q|--query="); match { + if match, value := optString(arg, "-q", "--query="); match { opts.Query = value - } else if match, value := optString(arg, "-f|--filter="); match { + } else if match, value := optString(arg, "-f", "--filter="); match { opts.Filter = &value - } else if match, value := optString(arg, "-d|--delimiter="); match { + } else if match, value := optString(arg, "-d", "--delimiter="); match { opts.Delimiter = delimiterRegexp(value) } else if match, value := optString(arg, "--prompt="); match { opts.Prompt = value - } else if match, value := optString(arg, "-n|--nth="); match { + } else if match, value := optString(arg, "-n", "--nth="); match { opts.Nth = splitNth(value) } else if match, value := optString(arg, "--with-nth="); match { opts.WithNth = splitNth(value) - } else if match, _ := optString(arg, "-s|--sort="); match { + } else if match, _ := optString(arg, "-s", "--sort="); match { opts.Sort = 1 // Don't care } else if match, value := optString(arg, "--toggle-sort="); match { keymap = checkToggleSort(keymap, value) From 6c99cc1700fda0c04500ee03b7e5f3ca22c7710c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 12:25:08 +0900 Subject: [PATCH 301/408] Add bind action for executing arbitrary command (#265) e.g. fzf --bind "ctrl-m:execute(less {})" fzf --bind "ctrl-t:execute[tmux new-window -d 'vim {}']" --- src/item.go | 9 +++++++-- src/options.go | 46 ++++++++++++++++++++++++++++++++++++++------- src/options_test.go | 26 +++++++++++++++++++++---- src/terminal.go | 33 +++++++++++++++++++++++--------- test/test_go.rb | 21 +++++++++++++++++++++ 5 files changed, 113 insertions(+), 22 deletions(-) diff --git a/src/item.go b/src/item.go index 7c2f94d..1eeb180 100644 --- a/src/item.go +++ b/src/item.go @@ -86,10 +86,15 @@ func (i *Item) Rank(cache bool) Rank { // AsString returns the original string func (i *Item) AsString() string { + return *i.StringPtr() +} + +// StringPtr returns the pointer to the original string +func (i *Item) StringPtr() *string { if i.origText != nil { - return *i.origText + return i.origText } - return *i.text + return i.text } func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset { diff --git a/src/options.go b/src/options.go index d969e51..948857c 100644 --- a/src/options.go +++ b/src/options.go @@ -117,6 +117,7 @@ type Options struct { ToggleSort bool Expect []int Keymap map[int]actionType + Execmap map[int]string PrintQuery bool ReadZero bool Sync bool @@ -157,6 +158,7 @@ func defaultOptions() *Options { ToggleSort: false, Expect: []int{}, Keymap: defaultKeymap(), + Execmap: make(map[int]string), PrintQuery: false, ReadZero: false, Sync: false, @@ -375,12 +377,22 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme return theme } -func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) { - for _, pairStr := range strings.Split(str, ",") { +func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) { + rx := regexp.MustCompile( + ":execute(\\([^)]*\\)|\\[[^\\]]*\\]|/[^/]*/|:[^:]*:|;[^;]*;|@[^@]*@|~[^~]*~|%[^%]*%|\\?[^?]*\\?)") + masked := rx.ReplaceAllStringFunc(str, func(src string) string { + return ":execute(" + strings.Repeat(" ", len(src)-10) + ")" + }) + + idx := 0 + for _, pairStr := range strings.Split(masked, ",") { + pairStr = str[idx : idx+len(pairStr)] + idx += len(pairStr) + 1 + fail := func() { errorExit("invalid key binding: " + pairStr) } - pair := strings.Split(pairStr, ":") + pair := strings.SplitN(pairStr, ":", 2) if len(pair) != 2 { fail() } @@ -455,10 +467,28 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in keymap[key] = actToggleSort toggleSort = true default: - errorExit("unknown action: " + act) + if isExecuteAction(act) { + keymap[key] = actExecute + execmap[key] = pair[1][8 : len(act)-1] + } else { + errorExit("unknown action: " + act) + } } } - return keymap, toggleSort + return keymap, execmap, toggleSort +} + +func isExecuteAction(str string) bool { + if !strings.HasPrefix(str, "execute") || len(str) < 9 { + return false + } + b := str[7] + e := str[len(str)-1] + if b == e && strings.ContainsAny(string(b), "/:;@~%?") || + b == '(' && e == ')' || b == '[' && e == ']' { + return true + } + return false } func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType { @@ -515,7 +545,8 @@ func parseOptions(opts *Options, allArgs []string) { case "--tiebreak": opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": - keymap, opts.ToggleSort = parseKeymap(keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) + keymap, opts.Execmap, opts.ToggleSort = + parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) case "--color": spec := optionalNextString(allArgs, &i) if len(spec) == 0 { @@ -629,7 +660,8 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--color="); match { opts.Theme = parseTheme(opts.Theme, value) } else if match, value := optString(arg, "--bind="); match { - keymap, opts.ToggleSort = parseKeymap(keymap, opts.ToggleSort, value) + keymap, opts.Execmap, opts.ToggleSort = + parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value) } else if match, value := optString(arg, "--history="); match { setHistory(value) } else if match, value := optString(arg, "--history-max="); match { diff --git a/src/options_test.go b/src/options_test.go index d356210..91e3754 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -136,11 +136,19 @@ func TestBind(t *testing.T) { t.Errorf("%d != %d", action, expected) } } + checkString := func(action string, expected string) { + if action != expected { + t.Errorf("%d != %d", action, expected) + } + } keymap := defaultKeymap() + execmap := make(map[int]string) check(actBeginningOfLine, keymap[curses.CtrlA]) - keymap, toggleSort := - parseKeymap(keymap, false, - "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down") + keymap, execmap, toggleSort := + parseKeymap(keymap, execmap, false, + "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ + "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute:less {}:,"+ + "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};") if !toggleSort { t.Errorf("toggleSort not set") } @@ -148,8 +156,18 @@ func TestBind(t *testing.T) { check(actToggleSort, keymap[curses.CtrlB]) check(actPageUp, keymap[curses.AltZ+'c']) check(actPageDown, keymap[curses.AltZ]) + check(actExecute, keymap[curses.F1]) + check(actExecute, keymap[curses.F2]) + check(actExecute, keymap[curses.F3]) + check(actExecute, keymap[curses.F4]) + checkString("ls {}", execmap[curses.F1]) + checkString("echo {}, {}, {}", execmap[curses.F2]) + checkString("echo '({})'", execmap[curses.F3]) + checkString("less {}", execmap[curses.F4]) + checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA]) + checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB]) - keymap, toggleSort = parseKeymap(keymap, false, "f1:abort") + keymap, execmap, toggleSort = parseKeymap(keymap, execmap, false, "f1:abort") if toggleSort { t.Errorf("toggleSort set") } diff --git a/src/terminal.go b/src/terminal.go index 50d380f..b0812fe 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "os" + "os/exec" "os/signal" "regexp" "sort" @@ -34,6 +35,7 @@ type Terminal struct { toggleSort bool expect []int keymap map[int]actionType + execmap map[int]string pressed int printQuery bool history *History @@ -119,6 +121,7 @@ const ( actToggleSort actPreviousHistory actNextHistory + actExecute ) func defaultKeymap() map[int]actionType { @@ -187,6 +190,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { toggleSort: opts.ToggleSort, expect: opts.Expect, keymap: opts.Keymap, + execmap: opts.Execmap, pressed: 0, printQuery: opts.PrintQuery, history: opts.History, @@ -587,6 +591,17 @@ func keyMatch(key int, event C.Event) bool { return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ } +func executeCommand(template string, current string) { + command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1) + cmd := exec.Command("sh", "-c", command) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + C.Endwin() + cmd.Run() + C.Refresh() +} + // Loop is called to start Terminal I/O func (t *Terminal) Loop() { <-t.startChan @@ -677,13 +692,7 @@ func (t *Terminal) Loop() { } selectItem := func(item *Item) bool { if _, found := t.selected[item.index]; !found { - var strptr *string - if item.origText != nil { - strptr = item.origText - } else { - strptr = item.text - } - t.selected[item.index] = selectedItem{time.Now(), strptr} + t.selected[item.index] = selectedItem{time.Now(), item.StringPtr()} return true } return false @@ -709,14 +718,20 @@ func (t *Terminal) Loop() { } action := t.keymap[event.Type] + mapkey := event.Type if event.Type == C.Rune { - code := int(event.Char) + int(C.AltZ) - if act, prs := t.keymap[code]; prs { + mapkey = int(event.Char) + int(C.AltZ) + if act, prs := t.keymap[mapkey]; prs { action = act } } switch action { case actIgnore: + case actExecute: + if t.cy >= 0 && t.cy < t.merger.Length() { + item := t.merger.Get(t.cy) + executeCommand(t.execmap[mapkey], item.AsString()) + } case actInvalid: t.mutex.Unlock() continue diff --git a/test/test_go.rb b/test/test_go.rb index 5521002..6a62731 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -594,6 +594,27 @@ class TestGoFZF < TestBase File.unlink history_file end + def test_execute + output = '/tmp/fzf-test-execute' + opts = %[--bind \\"alt-a:execute(echo '[{}]' >> #{output}),alt-b:execute[echo '({}), ({})' >> #{output}],C:execute:echo '({}), [{}], @{}@' >> #{output}:\\"] + tmux.send_keys "seq 100 | #{fzf opts}", :Enter + tmux.until { |lines| lines[-2].include? '100/100' } + tmux.send_keys :Escape, :a, :Escape, :a + tmux.send_keys :Up + tmux.send_keys :Escape, :b, :Escape, :b + tmux.send_keys :Up + tmux.send_keys :C + tmux.send_keys 'foobar' + tmux.until { |lines| lines[-2].include? '0/100' } + tmux.send_keys :Escape, :a, :Escape, :b, :Escape, :c + tmux.send_keys :Enter + readonce + assert_equal ['["1"]', '["1"]', '("2"), ("2")', '("2"), ("2")', '("3"), ["3"], @"3"@'], + File.readlines(output).map(&:chomp) + ensure + File.unlink output rescue nil + end + private def writelines path, lines File.unlink path while File.exists? path From 022435a90a64a4a20cc09d55c4f0c23435a8c0fe Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 23:36:49 +0900 Subject: [PATCH 302/408] More alternative notations for execute action execute(...) execute[...] execute~...~ execute!...! execute@...@ execute#...# execute$...$ execute%...% execute^...^ execute&...& execute*...* execute:...: execute;...; execute/.../ execute|...| --- src/options.go | 14 ++++++++++---- src/options_test.go | 7 +++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/options.go b/src/options.go index 948857c..fbeb9e4 100644 --- a/src/options.go +++ b/src/options.go @@ -377,10 +377,16 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme return theme } +var executeRegexp *regexp.Regexp + func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) { - rx := regexp.MustCompile( - ":execute(\\([^)]*\\)|\\[[^\\]]*\\]|/[^/]*/|:[^:]*:|;[^;]*;|@[^@]*@|~[^~]*~|%[^%]*%|\\?[^?]*\\?)") - masked := rx.ReplaceAllStringFunc(str, func(src string) string { + if executeRegexp == nil { + // Backreferences are not supported. + // "~!@#$%^&*:;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') + executeRegexp = regexp.MustCompile( + ":execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|:[^:]*:|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") + } + masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { return ":execute(" + strings.Repeat(" ", len(src)-10) + ")" }) @@ -484,7 +490,7 @@ func isExecuteAction(str string) bool { } b := str[7] e := str[len(str)-1] - if b == e && strings.ContainsAny(string(b), "/:;@~%?") || + if b == e && strings.ContainsAny(string(b), "~!@#$%^&*:;/|") || b == '(' && e == ')' || b == '[' && e == ']' { return true } diff --git a/src/options_test.go b/src/options_test.go index 91e3754..297acbf 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -1,6 +1,7 @@ package fzf import ( + "fmt" "testing" "github.com/junegunn/fzf/src/curses" @@ -167,6 +168,12 @@ func TestBind(t *testing.T) { checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA]) checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB]) + for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ':', ';', '/'} { + keymap, execmap, toggleSort = + parseKeymap(keymap, execmap, false, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) + checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) + } + keymap, execmap, toggleSort = parseKeymap(keymap, execmap, false, "f1:abort") if toggleSort { t.Errorf("toggleSort set") From e287bd7f04fd31b237d24616167074ada16e8bf1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 23:44:42 +0900 Subject: [PATCH 303/408] Fix Travis CI build --- test/test_go.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_go.rb b/test/test_go.rb index 6a62731..a12ed4d 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -787,6 +787,7 @@ class TestBash < TestBase include CompletionTest def new_shell + tmux.prepare tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter tmux.prepare end From 7db53e645923944a711e81515a2fcc5bb014f81f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 15 Jun 2015 01:26:18 +0900 Subject: [PATCH 304/408] Add synonyms for some keys to be used with --bind and --toggle-sort enter (return), space, tab, btab, esc, up, down, left, right --- src/options.go | 58 +++++++++++++++++++++++++++++++++------------ src/options_test.go | 28 ++++++++++++++++------ 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/options.go b/src/options.go index fbeb9e4..2bd013d 100644 --- a/src/options.go +++ b/src/options.go @@ -262,7 +262,7 @@ func isAlphabet(char uint8) bool { return char >= 'a' && char <= 'z' } -func parseKeyChords(str string, message string) []int { +func parseKeyChords(str string, message string, bind bool) []int { if len(str) == 0 { errorExit(message) } @@ -278,16 +278,44 @@ func parseKeyChords(str string, message string) []int { continue // ignore } lkey := strings.ToLower(key) - if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { - chords = append(chords, curses.CtrlA+int(lkey[5])-'a') - } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { - chords = append(chords, curses.AltA+int(lkey[4])-'a') - } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' { - chords = append(chords, curses.F1+int(key[1])-'1') - } else if utf8.RuneCountInString(key) == 1 { - chords = append(chords, curses.AltZ+int([]rune(key)[0])) - } else { - errorExit("unsupported key: " + key) + chord := 0 + if bind { + switch lkey { + case "up": + chord = curses.Up + case "down": + chord = curses.Down + case "left": + chord = curses.Left + case "right": + chord = curses.Right + case "enter", "return": + chord = curses.CtrlM + case "space": + chord = curses.AltZ + int(' ') + case "tab": + chord = curses.Tab + case "btab": + chord = curses.BTab + case "esc": + chord = curses.ESC + } + } + if chord == 0 { + if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { + chord = curses.CtrlA + int(lkey[5]) - 'a' + } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { + chord = curses.AltA + int(lkey[4]) - 'a' + } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' { + chord = curses.F1 + int(key[1]) - '1' + } else if utf8.RuneCountInString(key) == 1 { + chord = curses.AltZ + int([]rune(key)[0]) + } else { + errorExit("unsupported key: " + key) + } + } + if chord > 0 { + chords = append(chords, chord) } } return chords @@ -402,7 +430,7 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b if len(pair) != 2 { fail() } - keys := parseKeyChords(pair[0], "key name required") + keys := parseKeyChords(pair[0], "key name required", true) if len(keys) != 1 { fail() } @@ -498,7 +526,7 @@ func isExecuteAction(str string) bool { } func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType { - keys := parseKeyChords(str, "key name required") + keys := parseKeyChords(str, "key name required", true) if len(keys) != 1 { errorExit("multiple keys specified") } @@ -547,7 +575,7 @@ func parseOptions(opts *Options, allArgs []string) { filter := nextString(allArgs, &i, "query string required") opts.Filter = &filter case "--expect": - opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") + opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required", false) case "--tiebreak": opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": @@ -660,7 +688,7 @@ func parseOptions(opts *Options, allArgs []string) { keymap = checkToggleSort(keymap, value) opts.ToggleSort = true } else if match, value := optString(arg, "--expect="); match { - opts.Expect = parseKeyChords(value, "key names required") + opts.Expect = parseKeyChords(value, "key names required", false) } else if match, value := optString(arg, "--tiebreak="); match { opts.Tiebreak = parseTiebreak(value) } else if match, value := optString(arg, "--color="); match { diff --git a/src/options_test.go b/src/options_test.go index 297acbf..630bde3 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -72,7 +72,7 @@ func TestIrrelevantNth(t *testing.T) { } func TestParseKeys(t *testing.T) { - keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "") + keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "", false) check := func(key int, expected int) { if key != expected { t.Errorf("%d != %d", key, expected) @@ -88,6 +88,20 @@ func TestParseKeys(t *testing.T) { check(keys[6], curses.CtrlA+'g'-'a') check(keys[7], curses.AltZ+'J') check(keys[8], curses.AltZ+'g') + + // Synonyms + keys = parseKeyChords("enter,return,space,tab,btab,esc,up,down,left,right", "", true) + check(len(keys), 10) + check(keys[0], curses.CtrlM) + check(keys[1], curses.CtrlM) + check(keys[2], curses.AltZ+' ') + check(keys[3], curses.Tab) + check(keys[4], curses.BTab) + check(keys[5], curses.ESC) + check(keys[6], curses.Up) + check(keys[7], curses.Down) + check(keys[8], curses.Left) + check(keys[9], curses.Right) } func TestParseKeysWithComma(t *testing.T) { @@ -97,36 +111,36 @@ func TestParseKeysWithComma(t *testing.T) { } } - keys := parseKeyChords(",", "") + keys := parseKeyChords(",", "", false) check(len(keys), 1) check(keys[0], curses.AltZ+',') - keys = parseKeyChords(",,a,b", "") + keys = parseKeyChords(",,a,b", "", false) check(len(keys), 3) check(keys[0], curses.AltZ+'a') check(keys[1], curses.AltZ+'b') check(keys[2], curses.AltZ+',') - keys = parseKeyChords("a,b,,", "") + keys = parseKeyChords("a,b,,", "", false) check(len(keys), 3) check(keys[0], curses.AltZ+'a') check(keys[1], curses.AltZ+'b') check(keys[2], curses.AltZ+',') - keys = parseKeyChords("a,,,b", "") + keys = parseKeyChords("a,,,b", "", false) check(len(keys), 3) check(keys[0], curses.AltZ+'a') check(keys[1], curses.AltZ+'b') check(keys[2], curses.AltZ+',') - keys = parseKeyChords("a,,,b,c", "") + keys = parseKeyChords("a,,,b,c", "", false) check(len(keys), 4) check(keys[0], curses.AltZ+'a') check(keys[1], curses.AltZ+'b') check(keys[2], curses.AltZ+'c') check(keys[3], curses.AltZ+',') - keys = parseKeyChords(",,,", "") + keys = parseKeyChords(",,,", "", false) check(len(keys), 1) check(keys[0], curses.AltZ+',') } From e720f56ea8f01d17baa600e0f212cee8ba7cc53c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 15 Jun 2015 22:45:31 +0900 Subject: [PATCH 305/408] Fix test code for docker build --- src/history_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/history_test.go b/src/history_test.go index 83e4029..fa6f106 100644 --- a/src/history_test.go +++ b/src/history_test.go @@ -1,6 +1,7 @@ package fzf import ( + "os/user" "testing" ) @@ -8,7 +9,12 @@ func TestHistory(t *testing.T) { maxHistory := 50 // Invalid arguments - for _, path := range []string{"/etc", "/proc", "/etc/sudoers"} { + user, _ := user.Current() + paths := []string{"/etc", "/proc"} + if user.Name != "root" { + paths = append(paths, "/etc/sudoers") + } + for _, path := range paths { if _, e := NewHistory(path, maxHistory); e == nil { t.Error("Error expected for: " + path) } From fa5b58968e67a7f046d67554174a1923cfb30ea9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 15 Jun 2015 23:00:38 +0900 Subject: [PATCH 306/408] Add alternative execute notation that does not require closing char This can be used to avoid parse errors that can happen when the command contains the closing character. Since the command does not finish at a certain character, the key binding should be the last one in the group. Suggested by @tiziano88. (#265) e.g. fzf --bind "ctrl-m:execute=COMMAND..." --bind ctrl-j:accept --- src/options.go | 12 ++++++++---- src/options_test.go | 4 +++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/options.go b/src/options.go index 2bd013d..25a87ac 100644 --- a/src/options.go +++ b/src/options.go @@ -412,7 +412,7 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b // Backreferences are not supported. // "~!@#$%^&*:;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile( - ":execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|:[^:]*:|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") + ":execute=.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|:[^:]*:|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") } masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { return ":execute(" + strings.Repeat(" ", len(src)-10) + ")" @@ -503,7 +503,11 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b default: if isExecuteAction(act) { keymap[key] = actExecute - execmap[key] = pair[1][8 : len(act)-1] + if pair[1][7] == '=' { + execmap[key] = pair[1][8:] + } else { + execmap[key] = pair[1][8 : len(act)-1] + } } else { errorExit("unknown action: " + act) } @@ -518,8 +522,8 @@ func isExecuteAction(str string) bool { } b := str[7] e := str[len(str)-1] - if b == e && strings.ContainsAny(string(b), "~!@#$%^&*:;/|") || - b == '(' && e == ')' || b == '[' && e == ']' { + if b == '=' || b == '(' && e == ')' || b == '[' && e == ']' || + b == e && strings.ContainsAny(string(b), "~!@#$%^&*:;/|") { return true } return false diff --git a/src/options_test.go b/src/options_test.go index 630bde3..162b8e4 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -163,7 +163,8 @@ func TestBind(t *testing.T) { parseKeymap(keymap, execmap, false, "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute:less {}:,"+ - "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};") + "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ + ",X:execute=foobar,Y:execute(baz)") if !toggleSort { t.Errorf("toggleSort not set") } @@ -181,6 +182,7 @@ func TestBind(t *testing.T) { checkString("less {}", execmap[curses.F4]) checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA]) checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB]) + checkString("foobar,Y:execute(baz)", execmap[curses.AltZ+'X']) for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ':', ';', '/'} { keymap, execmap, toggleSort = From 794ad5785ddd96e65e71767be2de201d0b154df2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 15 Jun 2015 23:11:22 +0900 Subject: [PATCH 307/408] Fix `.` to match newlines as well (#265) --- src/options.go | 2 +- src/options_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/options.go b/src/options.go index 25a87ac..7ecf764 100644 --- a/src/options.go +++ b/src/options.go @@ -412,7 +412,7 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b // Backreferences are not supported. // "~!@#$%^&*:;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile( - ":execute=.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|:[^:]*:|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") + "(?s):execute=.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|:[^:]*:|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") } masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { return ":execute(" + strings.Repeat(" ", len(src)-10) + ")" diff --git a/src/options_test.go b/src/options_test.go index 162b8e4..3c33ef4 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -164,7 +164,7 @@ func TestBind(t *testing.T) { "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute:less {}:,"+ "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ - ",X:execute=foobar,Y:execute(baz)") + ",X:execute=\nfoobar,Y:execute(baz)") if !toggleSort { t.Errorf("toggleSort not set") } @@ -182,7 +182,7 @@ func TestBind(t *testing.T) { checkString("less {}", execmap[curses.F4]) checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA]) checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB]) - checkString("foobar,Y:execute(baz)", execmap[curses.AltZ+'X']) + checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X']) for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ':', ';', '/'} { keymap, execmap, toggleSort = From 8677dbded15ffad32bdc4cc20db4152a376424f0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 15 Jun 2015 23:25:00 +0900 Subject: [PATCH 308/408] Change alternative notation for execute action (#265) e.g. fzf --bind "ctrl-m:execute:COMMAND..." --bind ctrl-j:accept --- src/options.go | 10 +++++----- src/options_test.go | 6 +++--- test/test_go.rb | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/options.go b/src/options.go index 7ecf764..891ea1c 100644 --- a/src/options.go +++ b/src/options.go @@ -410,9 +410,9 @@ var executeRegexp *regexp.Regexp func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) { if executeRegexp == nil { // Backreferences are not supported. - // "~!@#$%^&*:;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') + // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile( - "(?s):execute=.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|:[^:]*:|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") + "(?s):execute:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") } masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { return ":execute(" + strings.Repeat(" ", len(src)-10) + ")" @@ -503,7 +503,7 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b default: if isExecuteAction(act) { keymap[key] = actExecute - if pair[1][7] == '=' { + if pair[1][7] == ':' { execmap[key] = pair[1][8:] } else { execmap[key] = pair[1][8 : len(act)-1] @@ -522,8 +522,8 @@ func isExecuteAction(str string) bool { } b := str[7] e := str[len(str)-1] - if b == '=' || b == '(' && e == ')' || b == '[' && e == ']' || - b == e && strings.ContainsAny(string(b), "~!@#$%^&*:;/|") { + if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || + b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { return true } return false diff --git a/src/options_test.go b/src/options_test.go index 3c33ef4..dee1c0d 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -162,9 +162,9 @@ func TestBind(t *testing.T) { keymap, execmap, toggleSort := parseKeymap(keymap, execmap, false, "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ - "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute:less {}:,"+ + "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ - ",X:execute=\nfoobar,Y:execute(baz)") + ",X:execute:\nfoobar,Y:execute(baz)") if !toggleSort { t.Errorf("toggleSort not set") } @@ -184,7 +184,7 @@ func TestBind(t *testing.T) { checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB]) checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X']) - for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ':', ';', '/'} { + for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { keymap, execmap, toggleSort = parseKeymap(keymap, execmap, false, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) diff --git a/test/test_go.rb b/test/test_go.rb index a12ed4d..5c814d0 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -596,7 +596,7 @@ class TestGoFZF < TestBase def test_execute output = '/tmp/fzf-test-execute' - opts = %[--bind \\"alt-a:execute(echo '[{}]' >> #{output}),alt-b:execute[echo '({}), ({})' >> #{output}],C:execute:echo '({}), [{}], @{}@' >> #{output}:\\"] + opts = %[--bind \\"alt-a:execute(echo '[{}]' >> #{output}),alt-b:execute[echo '({}), ({})' >> #{output}],C:execute:echo '({}), [{}], @{}@' >> #{output}\\"] tmux.send_keys "seq 100 | #{fzf opts}", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys :Escape, :a, :Escape, :a From 45bd323cab0fc6c81ccf7f06521bc3d8cc3b9402 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 16 Jun 2015 02:17:06 +0900 Subject: [PATCH 309/408] Allow binding CTRL-G and CTRL-Q --- src/curses/curses.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index fc0fef2..71df222 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -476,8 +476,12 @@ func GetChar() Event { }() switch _buf[0] { - case CtrlC, CtrlG, CtrlQ: + case CtrlC: return Event{CtrlC, 0, nil} + case CtrlG: + return Event{CtrlG, 0, nil} + case CtrlQ: + return Event{CtrlQ, 0, nil} case 127: return Event{CtrlH, 0, nil} case ESC: From d54a4fa223fa38415f272e7e937ccebefd39a388 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 16 Jun 2015 02:18:49 +0900 Subject: [PATCH 310/408] Add key name "bspace" for --bind (bspace != ctrl-h) --- src/curses/curses.go | 3 ++- src/options.go | 2 ++ src/terminal.go | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index 71df222..4dde288 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -51,6 +51,7 @@ const ( Mouse BTab + BSpace Del PgUp @@ -483,7 +484,7 @@ func GetChar() Event { case CtrlQ: return Event{CtrlQ, 0, nil} case 127: - return Event{CtrlH, 0, nil} + return Event{BSpace, 0, nil} case ESC: return escSequence(&sz) } diff --git a/src/options.go b/src/options.go index 891ea1c..425b19e 100644 --- a/src/options.go +++ b/src/options.go @@ -293,6 +293,8 @@ func parseKeyChords(str string, message string, bind bool) []int { chord = curses.CtrlM case "space": chord = curses.AltZ + int(' ') + case "bspace": + chord = curses.BSpace case "tab": chord = curses.Tab case "btab": diff --git a/src/terminal.go b/src/terminal.go index b0812fe..d17ade1 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -137,6 +137,7 @@ func defaultKeymap() map[int]actionType { keymap[C.CtrlE] = actEndOfLine keymap[C.CtrlF] = actForwardChar keymap[C.CtrlH] = actBackwardDeleteChar + keymap[C.BSpace] = actBackwardDeleteChar keymap[C.Tab] = actToggleDown keymap[C.BTab] = actToggleUp keymap[C.CtrlJ] = actDown From fe4e452d68435aa5e288b5e6364862a5fc6551c0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 16 Jun 2015 23:14:57 +0900 Subject: [PATCH 311/408] Add --cycle option for cyclic scrolling Close #266 --- src/options.go | 7 +++++++ src/terminal.go | 20 +++++++++++++++++--- test/test_go.rb | 19 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/options.go b/src/options.go index 425b19e..31a5c17 100644 --- a/src/options.go +++ b/src/options.go @@ -37,6 +37,7 @@ const usage = `usage: fzf [options] --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --black Use black background --reverse Reverse orientation + --cycle Enable cyclic scroll --no-hscroll Disable horizontal scroll --inline-info Display finder info inline with the query --prompt=STR Input prompt (default: '> ') @@ -107,6 +108,7 @@ type Options struct { Theme *curses.ColorTheme Black bool Reverse bool + Cycle bool Hscroll bool InlineInfo bool Prompt string @@ -148,6 +150,7 @@ func defaultOptions() *Options { Theme: defaultTheme(), Black: false, Reverse: false, + Cycle: false, Hscroll: true, InlineInfo: false, Prompt: "> ", @@ -637,6 +640,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Reverse = true case "--no-reverse": opts.Reverse = false + case "--cycle": + opts.Cycle = true + case "--no-cycle": + opts.Cycle = false case "--hscroll": opts.Hscroll = true case "--no-hscroll": diff --git a/src/terminal.go b/src/terminal.go index d17ade1..f213195 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -39,6 +39,7 @@ type Terminal struct { pressed int printQuery bool history *History + cycle bool count int progress int reading bool @@ -195,6 +196,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { pressed: 0, printQuery: opts.PrintQuery, history: opts.History, + cycle: opts.Cycle, merger: EmptyMerger, selected: make(map[uint32]selectedItem), reqBox: util.NewEventBox(), @@ -945,10 +947,22 @@ func (t *Terminal) constrain() { func (t *Terminal) vmove(o int) { if t.reverse { - t.vset(t.cy - o) - } else { - t.vset(t.cy + o) + o *= -1 } + dest := t.cy + o + if t.cycle { + max := t.merger.Length() - 1 + if dest > max { + if t.cy == max { + dest = 0 + } + } else if dest < 0 { + if t.cy == 0 { + dest = max + } + } + } + t.vset(dest) } func (t *Terminal) vset(o int) bool { diff --git a/test/test_go.rb b/test/test_go.rb index 5c814d0..1108afe 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -615,6 +615,25 @@ class TestGoFZF < TestBase File.unlink output rescue nil end + def test_cycle + tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter + tmux.until { |lines| lines[-2].include? '8/8' } + tmux.send_keys :Down + tmux.until { |lines| lines[-10].start_with? '>' } + tmux.send_keys :Down + tmux.until { |lines| lines[-9].start_with? '>' } + tmux.send_keys :PgUp + tmux.until { |lines| lines[-10].start_with? '>' } + tmux.send_keys :PgUp + tmux.until { |lines| lines[-3].start_with? '>' } + tmux.send_keys :Up + tmux.until { |lines| lines[-4].start_with? '>' } + tmux.send_keys :PgDn + tmux.until { |lines| lines[-3].start_with? '>' } + tmux.send_keys :PgDn + tmux.until { |lines| lines[-10].start_with? '>' } + end + private def writelines path, lines File.unlink path while File.exists? path From d261c36cde9ef933d008e4cd7005497ad18f2158 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 18 Jun 2015 00:42:38 +0900 Subject: [PATCH 312/408] Keep the spinner spinning even when the source stream is idle --- src/terminal.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/terminal.go b/src/terminal.go index f213195..aca8aad 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -197,6 +197,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { printQuery: opts.PrintQuery, history: opts.History, cycle: opts.Cycle, + reading: true, merger: EmptyMerger, selected: make(map[uint32]selectedItem), reqBox: util.NewEventBox(), @@ -630,6 +631,20 @@ func (t *Terminal) Loop() { t.reqBox.Set(reqRedraw, nil) } }() + + // Keep the spinner spinning + go func() { + for { + t.mutex.Lock() + reading := t.reading + t.mutex.Unlock() + if !reading { + break + } + time.Sleep(spinnerDuration) + t.reqBox.Set(reqInfo, nil) + } + }() } exit := func(code int) { From b504c6eb39d1327235a5897bb446284ce393b8fe Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 18 Jun 2015 02:09:03 +0900 Subject: [PATCH 313/408] Avoid intermittent test failures by making sure that we're back on shell command-line --- test/test_go.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 1108afe..7376384 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -156,6 +156,7 @@ class TestBase < Minitest::Test File.read(tempname) ensure File.unlink tempname while File.exists?(tempname) + tmux.prepare end def fzf(*opts) @@ -672,7 +673,7 @@ module TestShell def test_alt_c tmux.prepare tmux.send_keys :Escape, :c, pane: 0 - lines = tmux.until(1) { |lines| lines.item_count > 0 } + lines = tmux.until(1) { |lines| lines.item_count > 0 && lines[-3][2..-1] } expected = lines[-3][2..-1] tmux.send_keys :Enter, pane: 1 tmux.prepare From 5e8d8dab824524b83e7146d8ee583dab5e796a95 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 18 Jun 2015 02:27:50 +0900 Subject: [PATCH 314/408] More key names for --bind --- src/options.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/options.go b/src/options.go index 31a5c17..8281ada 100644 --- a/src/options.go +++ b/src/options.go @@ -298,12 +298,28 @@ func parseKeyChords(str string, message string, bind bool) []int { chord = curses.AltZ + int(' ') case "bspace": chord = curses.BSpace + case "alt-bs", "alt-bspace": + chord = curses.AltBS case "tab": chord = curses.Tab case "btab": chord = curses.BTab case "esc": chord = curses.ESC + case "del": + chord = curses.Del + case "home": + chord = curses.Home + case "end": + chord = curses.End + case "pgup", "page-up": + chord = curses.PgUp + case "pgdn", "page-down": + chord = curses.PgDn + case "shift-left": + chord = curses.SLeft + case "shift-right": + chord = curses.SRight } } if chord == 0 { From a8b2c257cdd51181bec67eca80c7879787c49776 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 19 Jun 2015 00:31:48 +0900 Subject: [PATCH 315/408] Improve handling of key names Remember the exact string given as the key name so that it's possible to correctly handle synonyms and print the original string. --- src/options.go | 108 ++++++++++++++++++----------------- src/options_test.go | 136 ++++++++++++++++++++++++++------------------ src/terminal.go | 22 ++----- 3 files changed, 142 insertions(+), 124 deletions(-) diff --git a/src/options.go b/src/options.go index 8281ada..a3b7cc8 100644 --- a/src/options.go +++ b/src/options.go @@ -117,7 +117,7 @@ type Options struct { Exit0 bool Filter *string ToggleSort bool - Expect []int + Expect map[int]string Keymap map[int]actionType Execmap map[int]string PrintQuery bool @@ -159,7 +159,7 @@ func defaultOptions() *Options { Exit0: false, Filter: nil, ToggleSort: false, - Expect: []int{}, + Expect: make(map[int]string), Keymap: defaultKeymap(), Execmap: make(map[int]string), PrintQuery: false, @@ -265,7 +265,7 @@ func isAlphabet(char uint8) bool { return char >= 'a' && char <= 'z' } -func parseKeyChords(str string, message string, bind bool) []int { +func parseKeyChords(str string, message string) map[int]string { if len(str) == 0 { errorExit(message) } @@ -275,54 +275,51 @@ func parseKeyChords(str string, message string, bind bool) []int { tokens = append(tokens, ",") } - var chords []int + chords := make(map[int]string) for _, key := range tokens { if len(key) == 0 { continue // ignore } lkey := strings.ToLower(key) chord := 0 - if bind { - switch lkey { - case "up": - chord = curses.Up - case "down": - chord = curses.Down - case "left": - chord = curses.Left - case "right": - chord = curses.Right - case "enter", "return": - chord = curses.CtrlM - case "space": - chord = curses.AltZ + int(' ') - case "bspace": - chord = curses.BSpace - case "alt-bs", "alt-bspace": - chord = curses.AltBS - case "tab": - chord = curses.Tab - case "btab": - chord = curses.BTab - case "esc": - chord = curses.ESC - case "del": - chord = curses.Del - case "home": - chord = curses.Home - case "end": - chord = curses.End - case "pgup", "page-up": - chord = curses.PgUp - case "pgdn", "page-down": - chord = curses.PgDn - case "shift-left": - chord = curses.SLeft - case "shift-right": - chord = curses.SRight - } - } - if chord == 0 { + switch lkey { + case "up": + chord = curses.Up + case "down": + chord = curses.Down + case "left": + chord = curses.Left + case "right": + chord = curses.Right + case "enter", "return": + chord = curses.CtrlM + case "space": + chord = curses.AltZ + int(' ') + case "bspace", "bs": + chord = curses.BSpace + case "alt-bs", "alt-bspace": + chord = curses.AltBS + case "tab": + chord = curses.Tab + case "btab", "shift-tab": + chord = curses.BTab + case "esc": + chord = curses.ESC + case "del": + chord = curses.Del + case "home": + chord = curses.Home + case "end": + chord = curses.End + case "pgup", "page-up": + chord = curses.PgUp + case "pgdn", "page-down": + chord = curses.PgDn + case "shift-left": + chord = curses.SLeft + case "shift-right": + chord = curses.SRight + default: if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { chord = curses.CtrlA + int(lkey[5]) - 'a' } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { @@ -336,7 +333,7 @@ func parseKeyChords(str string, message string, bind bool) []int { } } if chord > 0 { - chords = append(chords, chord) + chords[chord] = key } } return chords @@ -428,6 +425,13 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme var executeRegexp *regexp.Regexp +func firstKey(keymap map[int]string) int { + for k := range keymap { + return k + } + return 0 +} + func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) { if executeRegexp == nil { // Backreferences are not supported. @@ -451,11 +455,11 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b if len(pair) != 2 { fail() } - keys := parseKeyChords(pair[0], "key name required", true) + keys := parseKeyChords(pair[0], "key name required") if len(keys) != 1 { fail() } - key := keys[0] + key := firstKey(keys) act := strings.ToLower(pair[1]) switch act { case "ignore": @@ -551,11 +555,11 @@ func isExecuteAction(str string) bool { } func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType { - keys := parseKeyChords(str, "key name required", true) + keys := parseKeyChords(str, "key name required") if len(keys) != 1 { errorExit("multiple keys specified") } - keymap[keys[0]] = actToggleSort + keymap[firstKey(keys)] = actToggleSort return keymap } @@ -600,7 +604,7 @@ func parseOptions(opts *Options, allArgs []string) { filter := nextString(allArgs, &i, "query string required") opts.Filter = &filter case "--expect": - opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required", false) + opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") case "--tiebreak": opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": @@ -717,7 +721,7 @@ func parseOptions(opts *Options, allArgs []string) { keymap = checkToggleSort(keymap, value) opts.ToggleSort = true } else if match, value := optString(arg, "--expect="); match { - opts.Expect = parseKeyChords(value, "key names required", false) + opts.Expect = parseKeyChords(value, "key names required") } else if match, value := optString(arg, "--tiebreak="); match { opts.Tiebreak = parseTiebreak(value) } else if match, value := optString(arg, "--color="); match { diff --git a/src/options_test.go b/src/options_test.go index dee1c0d..8e44585 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -72,77 +72,101 @@ func TestIrrelevantNth(t *testing.T) { } func TestParseKeys(t *testing.T) { - keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "", false) - check := func(key int, expected int) { - if key != expected { - t.Errorf("%d != %d", key, expected) + pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "") + check := func(i int, s string) { + if pairs[i] != s { + t.Errorf("%s != %s", pairs[i], s) } } - check(len(keys), 9) - check(keys[0], curses.CtrlZ) - check(keys[1], curses.AltZ) - check(keys[2], curses.F2) - check(keys[3], curses.AltZ+'@') - check(keys[4], curses.AltA) - check(keys[5], curses.AltZ+'!') - check(keys[6], curses.CtrlA+'g'-'a') - check(keys[7], curses.AltZ+'J') - check(keys[8], curses.AltZ+'g') + if len(pairs) != 9 { + t.Error(9) + } + check(curses.CtrlZ, "ctrl-z") + check(curses.AltZ, "alt-z") + check(curses.F2, "f2") + check(curses.AltZ+'@', "@") + check(curses.AltA, "Alt-a") + check(curses.AltZ+'!', "!") + check(curses.CtrlA+'g'-'a', "ctrl-G") + check(curses.AltZ+'J', "J") + check(curses.AltZ+'g', "g") // Synonyms - keys = parseKeyChords("enter,return,space,tab,btab,esc,up,down,left,right", "", true) - check(len(keys), 10) - check(keys[0], curses.CtrlM) - check(keys[1], curses.CtrlM) - check(keys[2], curses.AltZ+' ') - check(keys[3], curses.Tab) - check(keys[4], curses.BTab) - check(keys[5], curses.ESC) - check(keys[6], curses.Up) - check(keys[7], curses.Down) - check(keys[8], curses.Left) - check(keys[9], curses.Right) + pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") + if len(pairs) != 9 { + t.Error(9) + } + check(curses.CtrlM, "Return") + check(curses.AltZ+' ', "space") + check(curses.Tab, "tab") + check(curses.BTab, "btab") + check(curses.ESC, "esc") + check(curses.Up, "up") + check(curses.Down, "down") + check(curses.Left, "left") + check(curses.Right, "right") + + pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") + if len(pairs) != 11 { + t.Error(11) + } + check(curses.Tab, "Ctrl-I") + check(curses.PgUp, "page-up") + check(curses.PgDn, "Page-Down") + check(curses.Home, "Home") + check(curses.End, "End") + check(curses.AltBS, "Alt-BSpace") + check(curses.SLeft, "shift-left") + check(curses.SRight, "shift-right") + check(curses.BTab, "shift-tab") + check(curses.CtrlM, "Enter") + check(curses.BSpace, "bspace") } func TestParseKeysWithComma(t *testing.T) { - check := func(key int, expected int) { - if key != expected { - t.Errorf("%d != %d", key, expected) + checkN := func(a int, b int) { + if a != b { + t.Errorf("%d != %d", a, b) + } + } + check := func(pairs map[int]string, i int, s string) { + if pairs[i] != s { + t.Errorf("%s != %s", pairs[i], s) } } - keys := parseKeyChords(",", "", false) - check(len(keys), 1) - check(keys[0], curses.AltZ+',') + pairs := parseKeyChords(",", "") + checkN(len(pairs), 1) + check(pairs, curses.AltZ+',', ",") - keys = parseKeyChords(",,a,b", "", false) - check(len(keys), 3) - check(keys[0], curses.AltZ+'a') - check(keys[1], curses.AltZ+'b') - check(keys[2], curses.AltZ+',') + pairs = parseKeyChords(",,a,b", "") + checkN(len(pairs), 3) + check(pairs, curses.AltZ+'a', "a") + check(pairs, curses.AltZ+'b', "b") + check(pairs, curses.AltZ+',', ",") - keys = parseKeyChords("a,b,,", "", false) - check(len(keys), 3) - check(keys[0], curses.AltZ+'a') - check(keys[1], curses.AltZ+'b') - check(keys[2], curses.AltZ+',') + pairs = parseKeyChords("a,b,,", "") + checkN(len(pairs), 3) + check(pairs, curses.AltZ+'a', "a") + check(pairs, curses.AltZ+'b', "b") + check(pairs, curses.AltZ+',', ",") - keys = parseKeyChords("a,,,b", "", false) - check(len(keys), 3) - check(keys[0], curses.AltZ+'a') - check(keys[1], curses.AltZ+'b') - check(keys[2], curses.AltZ+',') + pairs = parseKeyChords("a,,,b", "") + checkN(len(pairs), 3) + check(pairs, curses.AltZ+'a', "a") + check(pairs, curses.AltZ+'b', "b") + check(pairs, curses.AltZ+',', ",") - keys = parseKeyChords("a,,,b,c", "", false) - check(len(keys), 4) - check(keys[0], curses.AltZ+'a') - check(keys[1], curses.AltZ+'b') - check(keys[2], curses.AltZ+'c') - check(keys[3], curses.AltZ+',') + pairs = parseKeyChords("a,,,b,c", "") + checkN(len(pairs), 4) + check(pairs, curses.AltZ+'a', "a") + check(pairs, curses.AltZ+'b', "b") + check(pairs, curses.AltZ+'c', "c") + check(pairs, curses.AltZ+',', ",") - keys = parseKeyChords(",,,", "", false) - check(len(keys), 1) - check(keys[0], curses.AltZ+',') + pairs = parseKeyChords(",,,", "") + checkN(len(pairs), 1) + check(pairs, curses.AltZ+',', ",") } func TestBind(t *testing.T) { diff --git a/src/terminal.go b/src/terminal.go index aca8aad..9c73197 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -33,10 +33,10 @@ type Terminal struct { multi bool sort bool toggleSort bool - expect []int + expect map[int]string keymap map[int]actionType execmap map[int]string - pressed int + pressed string printQuery bool history *History cycle bool @@ -193,7 +193,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { expect: opts.Expect, keymap: opts.Keymap, execmap: opts.Execmap, - pressed: 0, + pressed: "", printQuery: opts.PrintQuery, history: opts.History, cycle: opts.Cycle, @@ -257,17 +257,7 @@ func (t *Terminal) output() { fmt.Println(string(t.input)) } if len(t.expect) > 0 { - if t.pressed == 0 { - fmt.Println() - } else if util.Between(t.pressed, C.AltA, C.AltZ) { - fmt.Printf("alt-%c\n", t.pressed+'a'-C.AltA) - } else if util.Between(t.pressed, C.F1, C.F4) { - fmt.Printf("f%c\n", t.pressed+'1'-C.F1) - } else if util.Between(t.pressed, C.CtrlA, C.CtrlZ) { - fmt.Printf("ctrl-%c\n", t.pressed+'a'-C.CtrlA) - } else { - fmt.Printf("%c\n", t.pressed-C.AltZ) - } + fmt.Println(t.pressed) } if len(t.selected) == 0 { cnt := t.merger.Length() @@ -727,9 +717,9 @@ func (t *Terminal) Loop() { req(reqInfo) } } - for _, key := range t.expect { + for key, ret := range t.expect { if keyMatch(key, event) { - t.pressed = key + t.pressed = ret req(reqClose) break } From 3492c8b780e3082946903d4b8e7b566e2c3e662c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 19 Jun 2015 01:03:25 +0900 Subject: [PATCH 316/408] Rename --history-max to --history-size Considering HISTSIZE and HISTFILESIZE of bash --- src/options.go | 6 +++--- test/test_go.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/options.go b/src/options.go index a3b7cc8..de3f18e 100644 --- a/src/options.go +++ b/src/options.go @@ -44,7 +44,7 @@ const usage = `usage: fzf [options] --toggle-sort=KEY Key to toggle sort --bind=KEYBINDS Custom key bindings. Refer to the man page. --history=FILE History file - --history-max=N Maximum number of history entries (default: 1000) + --history-size=N Maximum number of history entries (default: 1000) Scripting -q, --query=STR Start the finder with the given query @@ -698,7 +698,7 @@ func parseOptions(opts *Options, allArgs []string) { opts.History = nil case "--history": setHistory(nextString(allArgs, &i, "history file path required")) - case "--history-max": + case "--history-size": setHistoryMax(nextInt(allArgs, &i, "history max size required")) case "--version": opts.Version = true @@ -731,7 +731,7 @@ func parseOptions(opts *Options, allArgs []string) { parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value) } else if match, value := optString(arg, "--history="); match { setHistory(value) - } else if match, value := optString(arg, "--history-max="); match { + } else if match, value := optString(arg, "--history-size="); match { setHistoryMax(atoi(value)) } else { errorExit("unknown option: " + arg) diff --git a/test/test_go.rb b/test/test_go.rb index 7376384..92f5a9e 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -552,7 +552,7 @@ class TestGoFZF < TestBase # History with limited number of entries File.unlink history_file rescue nil - opts = "--history=#{history_file} --history-max=4" + opts = "--history=#{history_file} --history-size=4" input = %w[00 11 22 33 44].map { |e| e + $/ } input.each do |keys| tmux.send_keys "seq 100 | #{fzf opts}", :Enter From c0d3faa84f24baa55fbb874080fc49f534941702 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 19 Jun 2015 01:06:56 +0900 Subject: [PATCH 317/408] Hide --toggle-sort from --help output Since the same can be now achieved with --bind KEY:toggle-sort --- src/options.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/options.go b/src/options.go index de3f18e..4fa7cc8 100644 --- a/src/options.go +++ b/src/options.go @@ -41,7 +41,6 @@ const usage = `usage: fzf [options] --no-hscroll Disable horizontal scroll --inline-info Display finder info inline with the query --prompt=STR Input prompt (default: '> ') - --toggle-sort=KEY Key to toggle sort --bind=KEYBINDS Custom key bindings. Refer to the man page. --history=FILE History file --history-size=N Maximum number of history entries (default: 1000) From d395ebd28f3be44e4fd6edfc38c919b80879bc15 Mon Sep 17 00:00:00 2001 From: Oliver Kiddle Date: Sun, 21 Jun 2015 00:44:27 +0200 Subject: [PATCH 318/408] use vi-fetch-history on zsh to get history line In addition to being simpler, it allows subsequent up/down history or accept-line-and-down-history widgets to work. Also allow for find being and alias if alias expansion after command is enabled. --- shell/key-bindings.zsh | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 86630d0..fecef20 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -26,7 +26,7 @@ bindkey '^T' fzf-file-widget # ALT-C - cd into the selected directory fzf-cd-widget() { - cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + cd "${$(command \find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m):-.}" zle reset-prompt } @@ -36,16 +36,10 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected restore_no_bang_hist - if selected=$(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then - num=$(echo "$selected" | head -n1 | awk '{print $1}' | sed 's/[^0-9]//g') + if selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER") ); then + num=$selected[1] if [ -n "$num" ]; then - LBUFFER=!$num - if setopt | grep nobanghist > /dev/null; then - restore_no_bang_hist=1 - unsetopt no_bang_hist - fi - zle expand-history - [ -n "$restore_no_bang_hist" ] && setopt no_bang_hist + zle vi-fetch-history -n $num fi fi zle redisplay From 8270f7f0cadf14c76feaadfb4068ccdf599a25e8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 21 Jun 2015 17:29:58 +0900 Subject: [PATCH 319/408] Rename --null to --read0 and undocument the option `--null` is ambiguous. For completeness' sake, we need both `--read0` and `--print0`. `--read0` only makes sense when the input contains multiline entries. However, fzf currently cannot correctly display multiline entries, I'm going to make `--read0` an undocumented feature. --- fzf | 4 ++-- src/options.go | 5 +++-- test/test_go.rb | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/fzf b/fzf index 78a02fb..17f86b5 100755 --- a/fzf +++ b/fzf @@ -206,10 +206,10 @@ class FZF @expect = true when /^--expect=(.*)$/ @expect = true - when '--toggle-sort', '--tiebreak', '--color', '--bind', '--history', '--history-max' + when '--toggle-sort', '--tiebreak', '--color', '--bind', '--history', '--history-size' argv.shift when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll', - '--inline-info', '--no-inline-info', '--null', /^--bind=(.*)$/, + '--inline-info', '--no-inline-info', '--read0', '--cycle', /^--bind=(.*)$/, /^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/, /^--history(-max)?=(.*)$/ # XXX else diff --git a/src/options.go b/src/options.go index 4fa7cc8..55e0543 100644 --- a/src/options.go +++ b/src/options.go @@ -50,7 +50,6 @@ const usage = `usage: fzf [options] -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. - --null Read null-byte separated strings from input --print-query Print query as the first line --expect=KEYS Comma-separated list of keys to complete fzf --sync Synchronous search for multi-staged filtering @@ -679,8 +678,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Exit0 = true case "+0", "--no-exit-0": opts.Exit0 = false - case "--null": + case "--read0": opts.ReadZero = true + case "--no-read0": + opts.ReadZero = false case "--print-query": opts.PrintQuery = true case "--no-print-query": diff --git a/test/test_go.rb b/test/test_go.rb index 92f5a9e..7bef8e6 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -518,10 +518,10 @@ class TestGoFZF < TestBase assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp end - def test_null + def test_read0 lines = `find .`.split($/) assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp - assert_equal lines.last, `find . -print0 | #{FZF} --null -e -f "^#{lines.last}$"`.chomp + assert_equal lines.last, `find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp end def test_select_all_deselect_all_toggle_all From a336494f5d8894f07c6d0297d408d59cd9185565 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 21 Jun 2015 17:40:36 +0900 Subject: [PATCH 320/408] 0.10.0 --- CHANGELOG.md | 50 ++++++++++++++ install | 2 +- man/man1/fzf.1 | 154 +++++++++++++++++++++++++++++++----------- shell/completion.bash | 4 +- src/constants.go | 2 +- src/options.go | 2 +- 6 files changed, 168 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 276ee19..50139d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,56 @@ CHANGELOG ========= +0.10.0 +------ + +### New features + +- More actions for `--bind` + - `select-all` + - `deselect-all` + - `toggle-all` + - `ignore` +- `execute(...)` action for running arbitrary command without leaving fzf + - `fzf --bind "ctrl-m:execute(less {})"` + - `fzf --bind "ctrl-t:execute(tmux new-window -d 'vim {}')"` + - If the command contains parentheses, use any of the follows alternative + notations to avoid parse errors + - `execute[...]` + - `execute~...~` + - `execute!...!` + - `execute@...@` + - `execute#...#` + - `execute$...$` + - `execute%...%` + - `execute^...^` + - `execute&...&` + - `execute*...*` + - `execute;...;` + - `execute/.../` + - `execute|...|` + - `execute:...` + - This is the special form that frees you from parse errors as it + does not expect the closing character + - The catch is that it should be the last one in the + comma-separated list +- Added support for optional search history + - `--history HISTORY_FILE` + - When used, `CTRL-N` and `CTRL-P` are automatically remapped to + `next-history` and `previous-history` + - `--history-size MAX_ENTRIES` (default: 1000) +- Cyclic scrolling can be enabled with `--cycle` +- Fixed the bug where the spinner was not spinning on idle input stream + - e.g. `sleep 100 | fzf` + +### Minor improvements/fixes + +- Added synonyms for key names that can be specified for `--bind`, + `--toggle-sort`, and `--expect` +- Fixed the color of multi-select marker on the current line +- Fixed to allow `^pattern$` in extended-search mode + + 0.9.13 ------ diff --git a/install b/install index 1b8d21b..0527772 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.9.13 +version=0.10.0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 733d767..c1ad1c2 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "June 2015" "fzf 0.9.13" "fzf - a command-line fuzzy finder" +.TH fzf 1 "June 2015" "fzf 0.10.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -52,10 +52,10 @@ Comma-separated list of field index expressions for limiting search scope. See \fBFIELD INDEX EXPRESSION\fR for details. .TP .BI "--with-nth=" "N[,..]" -Transform the item using the list of index expressions for search +Transform each item using index expressions within finder .TP .BI "-d, --delimiter=" "STR" -Field delimiter regex for \fI--nth\fR and \fI--with-nth\fR (default: AWK-style) +Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) .SS Search result .TP .B "+s, --no-sort" @@ -130,6 +130,9 @@ Use black background .B "--reverse" Reverse orientation .TP +.B "--cycle" +Enable cyclic scroll +.TP .B "--no-hscroll" Disable horizontal scroll .TP @@ -140,8 +143,7 @@ Display finder info inline with the query Input prompt (default: '> ') .TP .BI "--toggle-sort=" "KEY" -Key to toggle sort (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, -or any single character) +Key to toggle sort. For the list of the allowed key names, see \fB--bind\fR. .TP .BI "--bind=" "KEYBINDS" Comma-separated list of custom key bindings. Each key binding expression @@ -151,38 +153,109 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR .RE .RS -.B KEY: - \fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, or any single character +.B AVAILABLE KEYS: + \fIctrl-[a-z]\fR + \fIalt-[a-z]\fR + \fIf[1-4]\fR + \fIenter\fR (\fIreturn\fR) + \fIspace\fR + \fIbspace\fR (\fIbs\fR) + \fIalt-bspace\fR (\fIalt-bs\fR) + \fItab\fR + \fIbtab\fR (\fIshift-tab\fR) + \fIesc\fR + \fIdel\fR + \fIup\fR + \fIdown\fR + \fIleft\fR + \fIright\fR + \fIhome\fR + \fIend\fR + \fIpgup\fR (\fIpage-up\fR) + \fIpgdn\fR (\fIpage-down\fR) + \fIshift-left\fR + \fIshift-right\fR + or any single character .RE .RS -.B ACTION: - abort - accept - backward-char - backward-delete-char - backward-kill-word - backward-word - beginning-of-line - clear-screen - delete-char - down - end-of-line - forward-char - forward-word - kill-line (not bound) - kill-word - page-down - page-up - toggle (not bound) - toggle-down - toggle-sort (not bound; equivalent to \fB--toggle-sort\fR) - toggle-up - unix-line-discard - unix-word-rubout - up - yank + \fBACTION: DEFAULT BINDINGS: + \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR + \fBaccept\fR \fIctrl-m (enter)\fR + \fBbackward-char\fR \fIctrl-b left\fR + \fBbackward-delete-char\fR \fIctrl-h bspace\fR + \fBbackward-kill-word\fR \fIalt-bs\fR + \fBbackward-word\fR \fIalt-b shift-left\fR + \fBbeginning-of-line\fR \fIctrl-a home\fR + \fBclear-screen\fR \fIctrl-l\fR + \fBdelete-char\fR \fIctrl-d del\fR + \fBdeselect-all\fR + \fBdown\fR \fIctrl-j ctrl-n down\fR + \fBend-of-line\fR \fIctrl-e end\fR + \fBexecute(...)\fR (see below for the details) + \fBforward-char\fR \fIctrl-f right\fR + \fBforward-word\fR \fIalt-f shift-right\fR + \fBignore\fR + \fBkill-line\fR + \fBkill-word\fR \fIalt-d\fR + \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) + \fBpage-down\fR \fIpgdn\fR + \fBpage-up\fR \fIpgup\fR + \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) + \fBselect-all\fR + \fBtoggle\fR + \fBtoggle-all\fR + \fBtoggle-down\fR \fIctrl-i (tab)\fR + \fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR) + \fBtoggle-up\fR \fIbtab (shift-tab)\fR + \fBunix-line-discard\fR \fIctrl-u\fR + \fBunix-word-rubout\fR \fIctrl-w\fR + \fBup\fR \fIctrl-k ctrl-p up\fR + \fByank\fR \fIctrl-y\fR .RE + +.RS +With \fBexecute(...)\fR action, you can execute arbitrary commands without +leaving fzf. For example, you can turn fzf into a simple file browser by +binding \fBenter\fR key to \fBless\fR command like follows. + +.RS +\fBfzf --bind "enter:execute(less {})"\fR +.RE + +\fB{}\fR is the placeholder for the double-quoted string of the current line. +If the command contains parentheses, you can use any of the following +alternative notations to avoid parse errors. + + \fBexecute[...]\fR + \fBexecute~...~\fR + \fBexecute!...!\fR + \fBexecute@...@\fR + \fBexecute#...#\fR + \fBexecute$...$\fR + \fBexecute%...%\fR + \fBexecute^...^\fR + \fBexecute&...&\fR + \fBexecute*...*\fR + \fBexecute;...;\fR + \fBexecute/.../\fR + \fBexecute|...|\fR + \fBexecute:...\fR +.RS +This is the special form that frees you from parse errors as it does not expect +the closing character. The catch is that it should be the last one in the +comma-separated list. +.RE +.RE +.TP +.BI "--history=" "HISTORY_FILE" +Load search history from the specified file and update the file on completion. +When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to +\fBnext-history\fR and \fBprevious-history\fR. +.TP +.BI "--history-size=" "N" +Maximum number of entries in the history file (default: 1000). The file is +automatically truncated when the number of the lines exceeds the value. .SS Scripting .TP .BI "-q, --query=" "STR" @@ -202,10 +275,9 @@ fzf becomes a fuzzy-version of grep. Print query as the first line .TP .BI "--expect=" "KEY[,..]" -Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, -or any single character) that can be used to complete fzf in addition to the -default enter key. When this option is set, fzf will print the name of the key -pressed as the first line of its output (or as the second line if +Comma-separated list of keys that can be used to complete fzf in addition to +the default enter key. When this option is set, fzf will print the name of the +key pressed as the first line of its output (or as the second line if \fB--print-query\fR is also used). The line will be empty if fzf is completed with the default enter key. .RS @@ -235,7 +307,7 @@ Default options. e.g. \fB--extended --ansi\fR .SH FIELD INDEX EXPRESSION A field index expression can be a non-zero integer or a range expression -([BEGIN]..[END]). \fI--nth\fR and \fI--with-nth\fR take a comma-separated list +([BEGIN]..[END]). \fB--nth\fR and \fB--with-nth\fR take a comma-separated list of field index expressions. .SS Examples @@ -258,7 +330,7 @@ of field index expressions. .SH EXTENDED SEARCH MODE -With \fI-x\fR or \fI--extended\fR option, fzf will start in "extended-search +With \fB-x\fR or \fB--extended\fR option, fzf will start in "extended-search mode". In this mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR @@ -278,8 +350,8 @@ from the result. .SS Extended-exact mode If you don't need fuzzy matching at all and do not wish to "quote" (prefixing -with ') every word, start fzf with \fI-e\fR or \fI--extended-exact\fR option -(instead of \fI-x\fR or \fI--extended\fR). +with ') every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option +(instead of \fB-x\fR or \fB--extended\fR). .SH AUTHOR Junegunn Choi (\fIjunegunn.c@gmail.com\fR) diff --git a/shell/completion.bash b/shell/completion.bash index 42d66c1..d9e7d44 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -46,9 +46,9 @@ _fzf_opts_completion() { --expect --toggle-sort --sync - --null + --cycle --history - --history-max" + --history-size" case "${prev}" in --tiebreak) diff --git a/src/constants.go b/src/constants.go index 87ba0f8..73ba451 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - Version = "0.9.13" + Version = "0.10.0" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond diff --git a/src/options.go b/src/options.go index 55e0543..4f79d33 100644 --- a/src/options.go +++ b/src/options.go @@ -23,7 +23,7 @@ const usage = `usage: fzf [options] -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 + --with-nth=N[,..] Transform item using index expressions within finder -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) +s, --no-sort Do not sort the result --tac Reverse the order of the input From 355d004895eb8cb7e6ca7f8cf0b095f381d6e41b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 21 Jun 2015 21:45:10 +0900 Subject: [PATCH 321/408] [neovim] Fix error with {'window': 'enew'} (#274) --- plugin/fzf.vim | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 48ad606..cc463de 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -248,13 +248,17 @@ function! s:calc_size(max, val) endif endfunction +function! s:getpos() + return {'tab': tabpagenr(), 'win': winnr()} +endfunction + function! s:split(dict) let directions = { \ 'up': ['topleft', 'resize', &lines], \ 'down': ['botright', 'resize', &lines], \ 'left': ['vertical topleft', 'vertical resize', &columns], \ 'right': ['vertical botright', 'vertical resize', &columns] } - let s:ptab = tabpagenr() + let s:ppos = s:getpos() try for [dir, triple] in items(directions) let val = get(a:dict, dir, '') @@ -282,19 +286,30 @@ function! s:execute_term(dict, command, temps) let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' } function! fzf.on_exit(id, code) - let tab = tabpagenr() - if bufnr('') == self.buf - " We use close instead of bd! since Vim does not close the split when - " there's no other listed buffer - close - endif - if s:ptab == tab - wincmd p + let pos = s:getpos() + let inplace = pos == s:ppos " {'window': 'enew'} + if !inplace + if bufnr('') == self.buf + " We use close instead of bd! since Vim does not close the split when + " there's no other listed buffer (nvim +'set nobuflisted') + close + endif + if pos.tab == s:ppos.tab + wincmd p + endif endif call s:pushd(self.dict) try redraw! call s:callback(self.dict, self.temps) + + if inplace && bufnr('') == self.buf + execute "normal! \" + " No other listed buffer + if bufnr('') == self.buf + bd! + endif + endif finally call s:popd(self.dict) endtry From 5254ee2e2a78f165d61edca1af6ce55e0dbe0921 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 22 Jun 2015 01:35:36 +0900 Subject: [PATCH 322/408] Update documentation (#277) --- README.md | 7 +++++++ man/man1/fzf.1 | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c5dd3f4..d460279 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,13 @@ such as: `^music .mp3$ sbtrkt !rmx` If you don't need fuzzy matching and do not wish to "quote" every word, start fzf with `-e` or `--extended-exact` option. +#### Environment variables + +- `FZF_DEFAULT_COMMAND` + - Default command to use when input is tty +- `FZF_DEFAULT_OPTS` + - Default options. e.g. `export FZF_DEFAULT_OPTS="--extended --cycle"` + Examples -------- diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index c1ad1c2..532e0df 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -297,7 +297,7 @@ e.g. \fBfzf --multi | fzf --sync\fR Default command to use when input is tty .TP .B FZF_DEFAULT_OPTS -Default options. e.g. \fB--extended --ansi\fR +Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR .SH EXIT STATUS .BR 0 " Normal exit" From 56100f0fa76f0e106ca54b7571f05d5837d61717 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 25 Jun 2015 23:54:05 +0900 Subject: [PATCH 323/408] [bash] Use `command \find` for ALT-C ALT-C can fail with the following aliases as pointed out in #272 alias find='noglob find' alias command='command ' --- shell/key-bindings.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index d543428..ee79cc3 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -29,7 +29,7 @@ __fzf_select_tmux__() { __fzf_cd__() { local dir - dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + dir=$(command \find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m) && printf 'cd %q' "$dir" } From 14e3b84073215e29f90f10e6ccce35704598e51d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 26 Jun 2015 00:14:36 +0900 Subject: [PATCH 324/408] [zsh] No need to define __fsel in non-interactive shell Since we now use fzf-tmux instead of tmux split-window --- shell/key-bindings.zsh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index fecef20..642a35e 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -1,5 +1,7 @@ # Key bindings # ------------ +if [[ $- =~ i ]]; then + # CTRL-T - Paste the selected file path(s) into the command line __fsel() { command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ @@ -15,8 +17,6 @@ __fzfcmd() { [ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } -if [[ $- =~ i ]]; then - fzf-file-widget() { LBUFFER="${LBUFFER}$(__fsel)" zle redisplay From 938151a834c1858ffdd6091cbcb42688a3784b6b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 26 Jun 2015 01:00:58 +0900 Subject: [PATCH 325/408] [shell] Add FZF_CTRL_T_COMMAND for CTRL-T Close #40 --- README.md | 5 +++-- shell/key-bindings.bash | 7 ++++--- shell/key-bindings.fish | 6 ++++-- shell/key-bindings.zsh | 5 +++-- test/test_go.rb | 21 +++++++++++++++++++++ 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d460279..3dc58e5 100644 --- a/README.md +++ b/README.md @@ -147,8 +147,9 @@ Key bindings for command line The install script will setup the following key bindings for bash, zsh, and fish. -- `CTRL-T` - Paste the selected file path(s) into the command line -- `CTRL-R` - Paste the selected command from history into the command line +- `CTRL-T` - Paste the selected files and directories onto the command line + - Set `FZF_CTRL_T_COMMAND` to override the default command +- `CTRL-R` - Paste the selected command from history onto the command line - Sort is disabled by default to respect chronological ordering - Press `CTRL-R` again to toggle sort - `ALT-C` - cd into the selected directory diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index ee79cc3..c04dc44 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -1,10 +1,11 @@ # Key bindings # ------------ __fzf_select__() { - command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + local cmd="${FZF_CTRL_T_COMMAND:-"command \\find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do + -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" + eval "$cmd" | fzf -m | while read item; do printf '%q ' "$item" done echo @@ -24,7 +25,7 @@ __fzf_select_tmux__() { else height="-l $height" fi - tmux split-window $height "cd $(printf %q "$PWD");bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" + tmux split-window $height "cd $(printf %q "$PWD"); FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" } __fzf_cd__() { diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index b86a4ef..c79d0fd 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -14,10 +14,12 @@ function fzf_key_bindings end function __fzf_ctrl_t - command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND " + command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) -m > $TMPDIR/fzf.result + -o -type l -print 2> /dev/null | sed 1d | cut -b3-" + eval $FZF_CTRL_T_COMMAND | eval (__fzfcmd) -m > $TMPDIR/fzf.result and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) commandline -f repaint rm -f $TMPDIR/fzf.result diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 642a35e..735aa50 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -4,10 +4,11 @@ if [[ $- =~ i ]]; then # CTRL-T - Paste the selected file path(s) into the command line __fsel() { - command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ + local cmd="${FZF_CTRL_T_COMMAND:-"command \\find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) -m | while read item; do + -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" + eval "$cmd" | $(__fzfcmd) -m | while read item; do printf '%q ' "$item" done echo diff --git a/test/test_go.rb b/test/test_go.rb index 7bef8e6..05966d4 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -148,6 +148,7 @@ class TestBase < Minitest::Test def setup ENV.delete 'FZF_DEFAULT_OPTS' + ENV.delete 'FZF_CTRL_T_COMMAND' ENV.delete 'FZF_DEFAULT_COMMAND' end @@ -651,6 +652,12 @@ module TestShell @tmux.kill end + def set_var name, val + tmux.prepare + tmux.send_keys "export #{name}='#{val}'", :Enter + tmux.prepare + end + def test_ctrl_t tmux.prepare tmux.send_keys 'C-t', pane: 0 @@ -670,6 +677,14 @@ module TestShell tmux.send_keys 'C-c', 'C-d' end + def test_ctrl_t_command + set_var "FZF_CTRL_T_COMMAND", "seq 100" + tmux.send_keys 'C-t', pane: 0 + lines = tmux.until(1) { |lines| lines.item_count == 100 } + tmux.send_keys :BTab, :BTab, :BTab, :Enter, pane: 1 + tmux.until(0) { |lines| lines[-1].include? '1 2 3' } + end + def test_alt_c tmux.prepare tmux.send_keys :Escape, :c, pane: 0 @@ -842,6 +857,12 @@ class TestFish < TestBase tmux.until { |lines| lines.empty? } end + def set_var name, val + tmux.prepare + tmux.send_keys "set -l #{name} '#{val}'", :Enter + tmux.prepare + end + def setup super @tmux = Tmux.new :fish From 11db046fc7bf1288b3237a7649441dfec6c19ab9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 27 Jun 2015 14:23:51 +0900 Subject: [PATCH 326/408] [neovim] Fix #281 - Properly close window with winnr 1 --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index cc463de..a149e79 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -249,7 +249,7 @@ function! s:calc_size(max, val) endfunction function! s:getpos() - return {'tab': tabpagenr(), 'win': winnr()} + return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$')} endfunction function! s:split(dict) From ec85fd552d2922244ae92377b634b77526bbd59d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 30 Jun 2015 13:17:48 +0900 Subject: [PATCH 327/408] Update README - how to use `ag` with CTRL-T --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 3dc58e5..64d9379 100644 --- a/README.md +++ b/README.md @@ -398,6 +398,9 @@ export FZF_DEFAULT_COMMAND='ag -l -g ""' # Now fzf (w/o pipe) will use ag instead of find fzf + +# To apply the command to CTRL-T as well +export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" ``` #### `git ls-tree` for fast traversal From a79d080ea8409893464f69a4cba9029a6ffcb484 Mon Sep 17 00:00:00 2001 From: Evgeny Vereshchagin Date: Fri, 3 Jul 2015 23:47:10 +0000 Subject: [PATCH 328/408] Fix g++: possible retry loop See http://unix.stackexchange.com/q/213432/120177 --- shell/completion.bash | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index d9e7d44..1e03b07 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -74,9 +74,10 @@ _fzf_opts_completion() { } _fzf_handle_dynamic_completion() { - local cmd orig ret + local cmd orig ret orig_cmd cmd="$1" shift + orig_cmd="$1" orig=$(eval "echo \$_fzf_orig_completion_$cmd") if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then @@ -84,7 +85,7 @@ _fzf_handle_dynamic_completion() { elif [ -n "$_fzf_completion_loader" ]; then _completion_loader "$@" ret=$? - eval $(complete | \grep "\-F.* $cmd$" | _fzf_orig_completion_filter) + eval $(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter) source $BASH_SOURCE return $ret fi From 392da53f5380907fdc7c21f398612be82f7dc3e0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 13 Jul 2015 00:22:13 +0900 Subject: [PATCH 329/408] [bash] Make CTRL-R work when histexpand is unset (#286) Note that it still can't handle properly multi-line commands. Thanks to @jpcirrus for the bug report and the fix. --- shell/key-bindings.bash | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index c04dc44..0bfd50d 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -34,13 +34,19 @@ __fzf_cd__() { -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m) && printf 'cd %q' "$dir" } -__fzf_history__() { +__fzf_history__() ( local line + shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= history | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | - \grep '^ *[0-9]') && sed 's/ *\([0-9]*\)\** .*/!\1/' <<< "$line" -} + \grep '^ *[0-9]') && + if [[ $- =~ H ]]; then + sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" + else + sed 's/^ *\([0-9]*\)\** *//' <<< "$line" + fi +) __use_tmux=0 [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1 From b0f491d3c3a3463b88b95c98f2a08abfdcccb087 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 13 Jul 2015 19:24:22 +0900 Subject: [PATCH 330/408] Fix travis CI build - Fix test failures on new fish 2.2.0 - Make timeout-based test cases more robust --- test/test_go.rb | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 05966d4..88dff86 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -312,7 +312,9 @@ class TestGoFZF < TestBase # However, the output must not be transformed if multi - tmux.send_keys :BTab, :BTab, :Enter + tmux.send_keys :BTab, :BTab + tmux.until { |lines| lines[-2].include?('(2)') } + tmux.send_keys :Enter assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/) else tmux.send_keys '^', '3' @@ -348,7 +350,9 @@ class TestGoFZF < TestBase [:'0', :'1', [:'1', :'0']].each do |opt| tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter tmux.until { |lines| lines.last =~ /^> 5/ } - tmux.send_keys :BTab, :BTab, :BTab, :Enter + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| lines[-2].include?('(3)') } + tmux.send_keys :Enter assert_equal ['5', '5', '15', '25'], readonce.split($/) end end @@ -365,7 +369,9 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-1] == '>' } tmux.send_keys 9 tmux.until { |lines| lines[-2] == ' 19/100' } - tmux.send_keys :BTab, :BTab, :BTab, :Enter + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| lines[-2].include?('(3)') } + tmux.send_keys :Enter tmux.until { |lines| lines[-1] == '>' } tmux.send_keys 'C-K', :Enter assert_equal ['1919'], readonce.split($/) @@ -374,7 +380,9 @@ class TestGoFZF < TestBase def test_tac tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.until { |lines| lines[-2].include? '1000/1000' } - tmux.send_keys :BTab, :BTab, :BTab, :Enter + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| lines[-2].include?('(3)') } + tmux.send_keys :Enter assert_equal %w[1000 999 998], readonce.split($/) end @@ -382,7 +390,9 @@ class TestGoFZF < TestBase tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.send_keys '99' - tmux.send_keys :BTab, :BTab, :BTab, :Enter + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| lines[-2].include?('(3)') } + tmux.send_keys :Enter assert_equal %w[99 999 998], readonce.split($/) end @@ -391,7 +401,9 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.send_keys '00' tmux.until { |lines| lines[-2].include? '10/1000' } - tmux.send_keys :BTab, :BTab, :BTab, :Enter + tmux.send_keys :BTab, :BTab, :BTab + tmux.until { |lines| lines[-2].include?('(3)') } + tmux.send_keys :Enter assert_equal %w[1000 900 800], readonce.split($/) end @@ -663,7 +675,9 @@ module TestShell tmux.send_keys 'C-t', pane: 0 lines = tmux.until(1) { |lines| lines.item_count > 1 } expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') - tmux.send_keys :BTab, :BTab, :Enter, pane: 1 + tmux.send_keys :BTab, :BTab, pane: 1 + tmux.until(1) { |lines| lines[-2].include?('(2)') } + tmux.send_keys :Enter, pane: 1 tmux.until(0) { |lines| lines[-1].include? expected } tmux.send_keys 'C-c' @@ -672,7 +686,9 @@ module TestShell tmux.send_keys 'C-t', pane: 0 lines = tmux.until(0) { |lines| lines.item_count > 1 } expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') - tmux.send_keys :BTab, :BTab, :Enter, pane: 0 + tmux.send_keys :BTab, :BTab, pane: 0 + tmux.until(0) { |lines| lines[-2].include?('(2)') } + tmux.send_keys :Enter, pane: 0 tmux.until(0) { |lines| lines[-1].include? expected } tmux.send_keys 'C-c', 'C-d' end @@ -681,7 +697,9 @@ module TestShell set_var "FZF_CTRL_T_COMMAND", "seq 100" tmux.send_keys 'C-t', pane: 0 lines = tmux.until(1) { |lines| lines.item_count == 100 } - tmux.send_keys :BTab, :BTab, :BTab, :Enter, pane: 1 + tmux.send_keys :BTab, :BTab, :BTab, pane: 1 + tmux.until(1) { |lines| lines[-2].include?('(3)') } + tmux.send_keys :Enter, pane: 1 tmux.until(0) { |lines| lines[-1].include? '1 2 3' } end @@ -725,7 +743,9 @@ module CompletionTest tmux.prepare tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 tmux.until(1) { |lines| lines.item_count > 0 } - tmux.send_keys :BTab, :BTab, :Enter + tmux.send_keys :BTab, :BTab + tmux.until(1) { |lines| lines[-2].include?('(2)') } + tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' lines[-1].include?('/tmp/fzf-test/10') && @@ -859,7 +879,7 @@ class TestFish < TestBase def set_var name, val tmux.prepare - tmux.send_keys "set -l #{name} '#{val}'", :Enter + tmux.send_keys "set -g #{name} '#{val}'", :Enter tmux.prepare end From a0e6147bb57f5a4ba4b9c78a4e2d14989376c1fe Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Jul 2015 19:18:23 +0900 Subject: [PATCH 331/408] Fix #292 - Allow binding of colon and comma --- src/options.go | 38 +++++++++++++++++++++++++++----------- src/options_test.go | 4 +++- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/options.go b/src/options.go index 4f79d33..1f53549 100644 --- a/src/options.go +++ b/src/options.go @@ -430,6 +430,11 @@ func firstKey(keymap map[int]string) int { return 0 } +const ( + escapedColon = 0 + escapedComma = 1 +) + func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) { if executeRegexp == nil { // Backreferences are not supported. @@ -440,10 +445,12 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { return ":execute(" + strings.Repeat(" ", len(src)-10) + ")" }) + masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) + masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) idx := 0 for _, pairStr := range strings.Split(masked, ",") { - pairStr = str[idx : idx+len(pairStr)] + origPairStr := str[idx : idx+len(pairStr)] idx += len(pairStr) + 1 fail := func() { @@ -453,13 +460,22 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b if len(pair) != 2 { fail() } - keys := parseKeyChords(pair[0], "key name required") - if len(keys) != 1 { - fail() + var key int + if len(pair[0]) == 1 && pair[0][0] == escapedColon { + key = ':' + curses.AltZ + } else if len(pair[0]) == 1 && pair[0][0] == escapedComma { + key = ',' + curses.AltZ + } else { + keys := parseKeyChords(pair[0], "key name required") + if len(keys) != 1 { + fail() + } + key = firstKey(keys) } - key := firstKey(keys) - act := strings.ToLower(pair[1]) - switch act { + + act := origPairStr[len(pair[0])+1 : len(origPairStr)] + actLower := strings.ToLower(act) + switch actLower { case "ignore": keymap[key] = actIgnore case "beginning-of-line": @@ -524,12 +540,12 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b keymap[key] = actToggleSort toggleSort = true default: - if isExecuteAction(act) { + if isExecuteAction(actLower) { keymap[key] = actExecute - if pair[1][7] == ':' { - execmap[key] = pair[1][8:] + if act[7] == ':' { + execmap[key] = act[8:] } else { - execmap[key] = pair[1][8 : len(act)-1] + execmap[key] = act[8 : len(act)-1] } } else { errorExit("unknown action: " + act) diff --git a/src/options_test.go b/src/options_test.go index 8e44585..28a8d9d 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -188,13 +188,15 @@ func TestBind(t *testing.T) { "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ - ",X:execute:\nfoobar,Y:execute(baz)") + ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") if !toggleSort { t.Errorf("toggleSort not set") } check(actKillLine, keymap[curses.CtrlA]) check(actToggleSort, keymap[curses.CtrlB]) check(actPageUp, keymap[curses.AltZ+'c']) + check(actAbort, keymap[curses.AltZ+',']) + check(actAccept, keymap[curses.AltZ+':']) check(actPageDown, keymap[curses.AltZ]) check(actExecute, keymap[curses.F1]) check(actExecute, keymap[curses.F2]) From c9abe1b1ff6e7ebbad9044941a0d7ea70b16802d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 18 Jul 2015 02:31:35 +0900 Subject: [PATCH 332/408] Show more specific error message on invalid binding --- src/options.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/options.go b/src/options.go index 1f53549..82dc6d9 100644 --- a/src/options.go +++ b/src/options.go @@ -453,12 +453,9 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b origPairStr := str[idx : idx+len(pairStr)] idx += len(pairStr) + 1 - fail := func() { - errorExit("invalid key binding: " + pairStr) - } pair := strings.SplitN(pairStr, ":", 2) - if len(pair) != 2 { - fail() + if len(pair) < 2 { + errorExit("bind action not specified: " + origPairStr) } var key int if len(pair[0]) == 1 && pair[0][0] == escapedColon { @@ -467,9 +464,6 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b key = ',' + curses.AltZ } else { keys := parseKeyChords(pair[0], "key name required") - if len(keys) != 1 { - fail() - } key = firstKey(keys) } From d459e9abce58d3b17c59a10bdcb018c8061bd3d2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 00:19:37 +0900 Subject: [PATCH 333/408] Add --header-file option --- man/man1/fzf.1 | 1 + src/curses/curses.go | 12 +++++++++--- src/options.go | 19 +++++++++++++++++++ src/terminal.go | 41 +++++++++++++++++++++++++++++++++-------- 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 532e0df..5d369e7 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -122,6 +122,7 @@ e.g. \fBfzf --color=bg+:24\fR \fBpointer \fRPointer to the current line \fBmarker \fRMulti-select marker \fBspinner \fRStreaming input indicator + \fBheader \fRHeader .RE .TP .B "--black" diff --git a/src/curses/curses.go b/src/curses/curses.go index 4dde288..95c6ad9 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -94,6 +94,7 @@ const ( ColInfo ColCursor ColSelected + ColHeader ColUser ) @@ -114,6 +115,7 @@ type ColorTheme struct { Info int16 Cursor int16 Selected int16 + Header int16 } type Event struct { @@ -164,7 +166,8 @@ func init() { Spinner: C.COLOR_GREEN, Info: C.COLOR_WHITE, Cursor: C.COLOR_RED, - Selected: C.COLOR_MAGENTA} + Selected: C.COLOR_MAGENTA, + Header: C.COLOR_CYAN} Dark256 = &ColorTheme{ UseDefault: true, Fg: 15, @@ -177,7 +180,8 @@ func init() { Spinner: 148, Info: 144, Cursor: 161, - Selected: 168} + Selected: 168, + Header: 110} Light256 = &ColorTheme{ UseDefault: true, Fg: 15, @@ -190,7 +194,8 @@ func init() { Spinner: 65, Info: 101, Cursor: 161, - Selected: 168} + Selected: 168, + Header: 31} } func attrColored(pair int, bold bool) C.int { @@ -308,6 +313,7 @@ func initPairs(theme *ColorTheme, black bool) { C.init_pair(ColInfo, C.short(theme.Info), bg) C.init_pair(ColCursor, C.short(theme.Cursor), darkBG) C.init_pair(ColSelected, C.short(theme.Selected), darkBG) + C.init_pair(ColHeader, C.short(theme.Header), bg) } func Close() { diff --git a/src/options.go b/src/options.go index 82dc6d9..4c6e9a0 100644 --- a/src/options.go +++ b/src/options.go @@ -2,6 +2,7 @@ package fzf import ( "fmt" + "io/ioutil" "os" "regexp" "strconv" @@ -44,6 +45,7 @@ const usage = `usage: fzf [options] --bind=KEYBINDS Custom key bindings. Refer to the man page. --history=FILE History file --history-size=N Maximum number of history entries (default: 1000) + --header-file=N Header file Scripting -q, --query=STR Start the finder with the given query @@ -122,6 +124,7 @@ type Options struct { ReadZero bool Sync bool History *History + Header []string Version bool } @@ -164,6 +167,7 @@ func defaultOptions() *Options { ReadZero: false, Sync: false, History: nil, + Header: make([]string, 0), Version: false} } @@ -413,6 +417,8 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme theme.Cursor = ansi case "marker": theme.Selected = ansi + case "header": + theme.Header = ansi default: fail() } @@ -571,6 +577,14 @@ func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType { return keymap } +func readHeaderFile(filename string) []string { + content, err := ioutil.ReadFile(filename) + if err != nil { + errorExit("failed to read header file: " + filename) + } + return strings.Split(strings.TrimSuffix(string(content), "\n"), "\n") +} + func parseOptions(opts *Options, allArgs []string) { keymap := make(map[int]actionType) var historyMax int @@ -710,6 +724,9 @@ func parseOptions(opts *Options, allArgs []string) { setHistory(nextString(allArgs, &i, "history file path required")) case "--history-size": setHistoryMax(nextInt(allArgs, &i, "history max size required")) + case "--header-file": + opts.Header = readHeaderFile( + nextString(allArgs, &i, "header file name required")) case "--version": opts.Version = true default: @@ -743,6 +760,8 @@ func parseOptions(opts *Options, allArgs []string) { setHistory(value) } else if match, value := optString(arg, "--history-size="); match { setHistoryMax(atoi(value)) + } else if match, value := optString(arg, "--header-file="); match { + opts.Header = readHeaderFile(value) } else { errorExit("unknown option: " + arg) } diff --git a/src/terminal.go b/src/terminal.go index 9c73197..e6e3c51 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -40,6 +40,7 @@ type Terminal struct { printQuery bool history *History cycle bool + header []string count int progress int reading bool @@ -197,6 +198,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { printQuery: opts.PrintQuery, history: opts.History, cycle: opts.Cycle, + header: opts.Header, reading: true, merger: EmptyMerger, selected: make(map[uint32]selectedItem), @@ -354,17 +356,39 @@ func (t *Terminal) printInfo() { C.CPrint(C.ColInfo, false, output) } +func (t *Terminal) printHeader() { + if len(t.header) == 0 { + return + } + for idx, lineStr := range t.header { + if !t.reverse { + idx = len(t.header) - idx - 1 + } + trimmed, colors := extractColor(&lineStr) + item := &Item{ + text: trimmed, + index: 0, + colors: colors, + rank: Rank{0, 0, 0}} + + line := idx + 2 + if t.inlineInfo { + line -= 1 + } + t.move(line, 2, true) + t.printHighlighted(item, false, C.ColHeader, 0, false) + } +} + func (t *Terminal) printList() { t.constrain() maxy := t.maxItems() count := t.merger.Length() - t.offset for i := 0; i < maxy; i++ { - var line int + line := i + 2 + len(t.header) if t.inlineInfo { - line = i + 1 - } else { - line = i + 2 + line -= 1 } t.move(line, 0, true) if i < count { @@ -606,6 +630,7 @@ func (t *Terminal) Loop() { t.placeCursor() C.Refresh() t.printInfo() + t.printHeader() t.mutex.Unlock() go func() { timer := time.NewTimer(initialDelay) @@ -883,9 +908,9 @@ func (t *Terminal) Loop() { if !t.reverse { my = C.MaxY() - my - 1 } - min := 2 + min := 2 + len(t.header) if t.inlineInfo { - min = 1 + min -= 1 } if me.S != 0 { // Scroll @@ -977,7 +1002,7 @@ func (t *Terminal) vset(o int) bool { func (t *Terminal) maxItems() int { if t.inlineInfo { - return C.MaxY() - 1 + return C.MaxY() - 1 - len(t.header) } - return C.MaxY() - 2 + return C.MaxY() - 2 - len(t.header) } From 8b2adba8d6807f50b65622b647207eb123af0102 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 00:47:14 +0900 Subject: [PATCH 334/408] Redraw of header on resize --- src/terminal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/terminal.go b/src/terminal.go index e6e3c51..43d2d8c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -551,6 +551,7 @@ func (t *Terminal) printAll() { t.printList() t.printPrompt() t.printInfo() + t.printHeader() } func (t *Terminal) refresh() { From d01db4862b7ab2d6b94cf3f437f503e4b8c862e9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 01:12:50 +0900 Subject: [PATCH 335/408] Update documentation --- man/man1/fzf.1 | 4 ++++ src/options.go | 38 +++++++++++++++++++------------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 5d369e7..8d10d38 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -257,6 +257,10 @@ When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to .BI "--history-size=" "N" Maximum number of entries in the history file (default: 1000). The file is automatically truncated when the number of the lines exceeds the value. +.TP +.BI "--header-file=" "FILE" +The content of the file will be printed as the "sticky" header. The file can +span multiple lines and can contain ANSI color codes. .SS Scripting .TP .BI "-q, --query=" "STR" diff --git a/src/options.go b/src/options.go index 4c6e9a0..e789d4e 100644 --- a/src/options.go +++ b/src/options.go @@ -24,37 +24,37 @@ const usage = `usage: fzf [options] -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 item using index expressions within finder + --with-nth=N[,..] Transform item using index expressions within finder -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) +s, --no-sort Do not sort the result - --tac Reverse the order of the input - --tiebreak=CRI Sort criterion when the scores are tied; + --tac Reverse the order of the input + --tiebreak=CRITERION Sort criterion when the scores are tied; [length|begin|end|index] (default: length) Interface -m, --multi Enable multi-select with tab/shift-tab - --ansi Enable processing of ANSI color codes - --no-mouse Disable mouse - --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors - --black Use black background - --reverse Reverse orientation - --cycle Enable cyclic scroll - --no-hscroll Disable horizontal scroll - --inline-info Display finder info inline with the query - --prompt=STR Input prompt (default: '> ') - --bind=KEYBINDS Custom key bindings. Refer to the man page. - --history=FILE History file - --history-size=N Maximum number of history entries (default: 1000) - --header-file=N Header file + --ansi Enable processing of ANSI color codes + --no-mouse Disable mouse + --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors + --black Use black background + --reverse Reverse orientation + --cycle Enable cyclic scroll + --no-hscroll Disable horizontal scroll + --inline-info Display finder info inline with the query + --prompt=STR Input prompt (default: '> ') + --bind=KEYBINDS Custom key bindings. Refer to the man page. + --history=FILE History file + --history-size=N Maximum number of history entries (default: 1000) + --header-file=FILE The file whose content to be printed as header 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 - --expect=KEYS Comma-separated list of keys to complete fzf - --sync Synchronous search for multi-staged filtering + --print-query Print query as the first line + --expect=KEYS Comma-separated list of keys to complete fzf + --sync Synchronous search for multi-staged filtering Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty From 18469b69549f1efd5294ebeb357d1773fc3a241e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 03:07:27 +0900 Subject: [PATCH 336/408] Adjust header color for dark color scheme --- src/curses/curses.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index 95c6ad9..9a1dc76 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -181,7 +181,7 @@ func init() { Info: 144, Cursor: 161, Selected: 168, - Header: 110} + Header: 109} Light256 = &ColorTheme{ UseDefault: true, Fg: 15, From f469c25730aaa711b8327be068514c944074cce4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 03:21:20 +0900 Subject: [PATCH 337/408] Add --header-lines option --- man/man1/fzf.1 | 5 +- src/chunklist.go | 18 ++++-- src/constants.go | 1 + src/core.go | 24 +++++++- src/options.go | 144 +++++++++++++++++++++++++-------------------- src/reader.go | 7 ++- src/reader_test.go | 2 +- src/terminal.go | 19 ++++++ 8 files changed, 143 insertions(+), 77 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 8d10d38..c42fe66 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -259,8 +259,11 @@ Maximum number of entries in the history file (default: 1000). The file is automatically truncated when the number of the lines exceeds the value. .TP .BI "--header-file=" "FILE" -The content of the file will be printed as the "sticky" header. The file can +The content of the file will be printed as the sticky header. The file can span multiple lines and can contain ANSI color codes. +.TP +.BI "--header-lines=" "N" +The first N lines of the input are treated as the sticky header. .SS Scripting .TP .BI "-q, --query=" "STR" diff --git a/src/chunklist.go b/src/chunklist.go index 52084f2..ee52d32 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -26,8 +26,13 @@ func NewChunkList(trans ItemBuilder) *ChunkList { trans: trans} } -func (c *Chunk) push(trans ItemBuilder, data *string, index int) { - *c = append(*c, trans(data, index)) +func (c *Chunk) push(trans ItemBuilder, data *string, index int) bool { + item := trans(data, index) + if item != nil { + *c = append(*c, item) + return true + } + return false } // IsFull returns true if the Chunk is full @@ -48,7 +53,7 @@ func CountItems(cs []*Chunk) int { } // Push adds the item to the list -func (cl *ChunkList) Push(data string) { +func (cl *ChunkList) Push(data string) bool { cl.mutex.Lock() defer cl.mutex.Unlock() @@ -57,8 +62,11 @@ func (cl *ChunkList) Push(data string) { cl.chunks = append(cl.chunks, &newChunk) } - cl.lastChunk().push(cl.trans, &data, cl.count) - cl.count++ + if cl.lastChunk().push(cl.trans, &data, cl.count) { + cl.count++ + return true + } + return false } // Snapshot returns immutable snapshot of the ChunkList diff --git a/src/constants.go b/src/constants.go index 73ba451..2692907 100644 --- a/src/constants.go +++ b/src/constants.go @@ -44,5 +44,6 @@ const ( EvtSearchNew EvtSearchProgress EvtSearchFin + EvtHeader EvtClose ) diff --git a/src/core.go b/src/core.go index c727708..e38908a 100644 --- a/src/core.go +++ b/src/core.go @@ -44,6 +44,7 @@ Reader -> EvtReadNew -> Matcher (restart) Terminal -> EvtSearchNew:bool -> Matcher (restart) Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchFin -> Terminal (update list) +Matcher -> EvtHeader -> Terminal (update header) */ // Run starts fzf @@ -83,8 +84,14 @@ func Run(opts *Options) { // Chunk list var chunkList *ChunkList + header := make([]string, 0, opts.HeaderLines) if len(opts.WithNth) == 0 { chunkList = NewChunkList(func(data *string, index int) *Item { + if len(header) < opts.HeaderLines { + header = append(header, *data) + eventBox.Set(EvtHeader, header) + return nil + } data, colors := ansiProcessor(data) return &Item{ text: data, @@ -94,6 +101,11 @@ func Run(opts *Options) { }) } else { chunkList = NewChunkList(func(data *string, index int) *Item { + if len(header) < opts.HeaderLines { + header = append(header, *data) + eventBox.Set(EvtHeader, header) + return nil + } tokens := Tokenize(data, opts.Delimiter) trans := Transform(tokens, opts.WithNth) item := Item{ @@ -113,7 +125,9 @@ func Run(opts *Options) { // Reader streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync if !streamingFilter { - reader := Reader{func(str string) { chunkList.Push(str) }, eventBox, opts.ReadZero} + reader := Reader{func(str string) bool { + return chunkList.Push(str) + }, eventBox, opts.ReadZero} go reader.ReadSource() } @@ -134,11 +148,12 @@ func Run(opts *Options) { if streamingFilter { reader := Reader{ - func(str string) { + func(str string) bool { item := chunkList.trans(&str, 0) - if pattern.MatchItem(item) { + if item != nil && pattern.MatchItem(item) { fmt.Println(*item.text) } + return false }, eventBox, opts.ReadZero} reader.ReadSource() } else { @@ -206,6 +221,9 @@ func Run(opts *Options) { terminal.UpdateProgress(val) } + case EvtHeader: + terminal.UpdateHeader(value.([]string), opts.HeaderLines) + case EvtSearchFin: switch val := value.(type) { case *Merger: diff --git a/src/options.go b/src/options.go index e789d4e..d8b2bd8 100644 --- a/src/options.go +++ b/src/options.go @@ -46,6 +46,7 @@ const usage = `usage: fzf [options] --history=FILE History file --history-size=N Maximum number of history entries (default: 1000) --header-file=FILE The file whose content to be printed as header + --header-lines=N The first N lines of the input are treated as header Scripting -q, --query=STR Start the finder with the given query @@ -94,38 +95,39 @@ const ( // Options stores the values of command-line options type Options struct { - Mode Mode - Case Case - Nth []Range - WithNth []Range - Delimiter *regexp.Regexp - Sort int - Tac bool - Tiebreak tiebreak - Multi bool - Ansi bool - Mouse bool - Theme *curses.ColorTheme - Black bool - Reverse bool - Cycle bool - Hscroll bool - InlineInfo bool - Prompt string - Query string - Select1 bool - Exit0 bool - Filter *string - ToggleSort bool - Expect map[int]string - Keymap map[int]actionType - Execmap map[int]string - PrintQuery bool - ReadZero bool - Sync bool - History *History - Header []string - Version bool + Mode Mode + Case Case + Nth []Range + WithNth []Range + Delimiter *regexp.Regexp + Sort int + Tac bool + Tiebreak tiebreak + Multi bool + Ansi bool + Mouse bool + Theme *curses.ColorTheme + Black bool + Reverse bool + Cycle bool + Hscroll bool + InlineInfo bool + Prompt string + Query string + Select1 bool + Exit0 bool + Filter *string + ToggleSort bool + Expect map[int]string + Keymap map[int]actionType + Execmap map[int]string + PrintQuery bool + ReadZero bool + Sync bool + History *History + Header []string + HeaderLines int + Version bool } func defaultTheme() *curses.ColorTheme { @@ -137,38 +139,39 @@ func defaultTheme() *curses.ColorTheme { func defaultOptions() *Options { return &Options{ - Mode: ModeFuzzy, - Case: CaseSmart, - Nth: make([]Range, 0), - WithNth: make([]Range, 0), - Delimiter: nil, - Sort: 1000, - Tac: false, - Tiebreak: byLength, - Multi: false, - Ansi: false, - Mouse: true, - Theme: defaultTheme(), - Black: false, - Reverse: false, - Cycle: false, - Hscroll: true, - InlineInfo: false, - Prompt: "> ", - Query: "", - Select1: false, - Exit0: false, - Filter: nil, - ToggleSort: false, - Expect: make(map[int]string), - Keymap: defaultKeymap(), - Execmap: make(map[int]string), - PrintQuery: false, - ReadZero: false, - Sync: false, - History: nil, - Header: make([]string, 0), - Version: false} + Mode: ModeFuzzy, + Case: CaseSmart, + Nth: make([]Range, 0), + WithNth: make([]Range, 0), + Delimiter: nil, + Sort: 1000, + Tac: false, + Tiebreak: byLength, + Multi: false, + Ansi: false, + Mouse: true, + Theme: defaultTheme(), + Black: false, + Reverse: false, + Cycle: false, + Hscroll: true, + InlineInfo: false, + Prompt: "> ", + Query: "", + Select1: false, + Exit0: false, + Filter: nil, + ToggleSort: false, + Expect: make(map[int]string), + Keymap: defaultKeymap(), + Execmap: make(map[int]string), + PrintQuery: false, + ReadZero: false, + Sync: false, + History: nil, + Header: make([]string, 0), + HeaderLines: 0, + Version: false} } func help(ok int) { @@ -724,9 +727,18 @@ func parseOptions(opts *Options, allArgs []string) { setHistory(nextString(allArgs, &i, "history file path required")) case "--history-size": setHistoryMax(nextInt(allArgs, &i, "history max size required")) + case "--no-header-file": + opts.Header = []string{} + case "--no-header-lines": + opts.HeaderLines = 0 case "--header-file": opts.Header = readHeaderFile( nextString(allArgs, &i, "header file name required")) + opts.HeaderLines = 0 + case "--header-lines": + opts.Header = []string{} + opts.HeaderLines = atoi( + nextString(allArgs, &i, "number of header lines required")) case "--version": opts.Version = true default: @@ -762,6 +774,10 @@ func parseOptions(opts *Options, allArgs []string) { setHistoryMax(atoi(value)) } else if match, value := optString(arg, "--header-file="); match { opts.Header = readHeaderFile(value) + opts.HeaderLines = 0 + } else if match, value := optString(arg, "--header-lines="); match { + opts.Header = []string{} + opts.HeaderLines = atoi(value) } else { errorExit("unknown option: " + arg) } diff --git a/src/reader.go b/src/reader.go index 356c2db..aab8b02 100644 --- a/src/reader.go +++ b/src/reader.go @@ -11,7 +11,7 @@ import ( // Reader reads from command or standard input type Reader struct { - pusher func(string) + pusher func(string) bool eventBox *util.EventBox delimNil bool } @@ -43,8 +43,9 @@ func (r *Reader) feed(src io.Reader) { if err == nil { line = line[:len(line)-1] } - r.pusher(line) - r.eventBox.Set(EvtReadNew, nil) + if r.pusher(line) { + r.eventBox.Set(EvtReadNew, nil) + } } if err != nil { break diff --git a/src/reader_test.go b/src/reader_test.go index 144a3ff..00b9e33 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -10,7 +10,7 @@ func TestReadFromCommand(t *testing.T) { strs := []string{} eb := util.NewEventBox() reader := Reader{ - pusher: func(s string) { strs = append(strs, s) }, + pusher: func(s string) bool { strs = append(strs, s); return true }, eventBox: eb} // Check EventBox diff --git a/src/terminal.go b/src/terminal.go index 43d2d8c..844574a 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -79,6 +79,7 @@ var _runeWidths = make(map[rune]int) const ( reqPrompt util.EventType = iota reqInfo + reqHeader reqList reqRefresh reqRedraw @@ -231,6 +232,22 @@ func (t *Terminal) UpdateCount(cnt int, final bool) { } } +// UpdateHeader updates the header +func (t *Terminal) UpdateHeader(header []string, lines int) { + t.mutex.Lock() + t.header = make([]string, lines) + copy(t.header, header) + if !t.reverse { + reversed := make([]string, lines) + for idx, str := range t.header { + reversed[lines-idx-1] = str + } + t.header = reversed + } + t.mutex.Unlock() + t.reqBox.Set(reqHeader, nil) +} + // UpdateProgress updates the search progress func (t *Terminal) UpdateProgress(progress float32) { t.mutex.Lock() @@ -686,6 +703,8 @@ func (t *Terminal) Loop() { t.printInfo() case reqList: t.printList() + case reqHeader: + t.printHeader() case reqRefresh: t.suppress = false case reqRedraw: From f71ea5f3ea469489fc979ee3c0cd2622d011befd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 13:45:38 +0900 Subject: [PATCH 338/408] Add test cases for header and fix corner cases --- src/options.go | 4 ++++ src/terminal.go | 18 ++++++++++------ test/test_go.rb | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/options.go b/src/options.go index d8b2bd8..17df221 100644 --- a/src/options.go +++ b/src/options.go @@ -784,6 +784,10 @@ func parseOptions(opts *Options, allArgs []string) { } } + if opts.HeaderLines < 0 { + errorExit("header lines must be a non-negative integer") + } + // Change default actions for CTRL-N / CTRL-P when --history is used if opts.History != nil { if _, prs := keymap[curses.CtrlP]; !prs { diff --git a/src/terminal.go b/src/terminal.go index 844574a..52c36cf 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -377,10 +377,18 @@ func (t *Terminal) printHeader() { if len(t.header) == 0 { return } + max := C.MaxY() for idx, lineStr := range t.header { if !t.reverse { idx = len(t.header) - idx - 1 } + line := idx + 2 + if t.inlineInfo { + line -= 1 + } + if line >= max { + break + } trimmed, colors := extractColor(&lineStr) item := &Item{ text: trimmed, @@ -388,10 +396,6 @@ func (t *Terminal) printHeader() { colors: colors, rank: Rank{0, 0, 0}} - line := idx + 2 - if t.inlineInfo { - line -= 1 - } t.move(line, 2, true) t.printHighlighted(item, false, C.ColHeader, 0, false) } @@ -993,6 +997,7 @@ func (t *Terminal) constrain() { t.offset = util.Max(0, count-height) t.cy = util.Constrain(t.offset+diffpos, 0, count-1) } + t.offset = util.Max(0, t.offset) } func (t *Terminal) vmove(o int) { @@ -1021,8 +1026,9 @@ func (t *Terminal) vset(o int) bool { } func (t *Terminal) maxItems() int { + max := C.MaxY() - 2 - len(t.header) if t.inlineInfo { - return C.MaxY() - 1 - len(t.header) + max += 1 } - return C.MaxY() - 2 - len(t.header) + return util.Max(max, 0) } diff --git a/test/test_go.rb b/test/test_go.rb index 88dff86..7effb5f 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -648,6 +648,61 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-10].start_with? '>' } end + def test_header_lines + tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5'}", :Enter + 2.times do + tmux.until do |lines| + lines[-2].include?('/90') && + lines[-3] == ' 1' && + lines[-4] == ' 2' && + lines[-13] == '> 15' + end + tmux.send_keys :Down + end + tmux.send_keys :Enter + assert_equal '15', readonce.chomp + end + + def test_header_lines_reverse + tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5 --reverse'}", :Enter + 2.times do + tmux.until do |lines| + lines[1].include?('/90') && + lines[2] == ' 1' && + lines[3] == ' 2' && + lines[12] == '> 15' + end + tmux.send_keys :Up + end + tmux.send_keys :Enter + assert_equal '15', readonce.chomp + end + + def test_header_lines_overflow + tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter + tmux.until { |lines| lines[-2].include?('0/0') } + tmux.send_keys :Enter + assert_equal '', readonce.chomp + end + + def test_header_file + tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__})"}", :Enter + header = File.readlines(__FILE__).take(5).map(&:strip) + tmux.until do |lines| + lines[-2].include?('100/100') && + lines[-7..-3].map(&:strip) == header + end + end + + def test_header_file_reverse + tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__}) --reverse"}", :Enter + header = File.readlines(__FILE__).take(5).map(&:strip) + tmux.until do |lines| + lines[1].include?('100/100') && + lines[2..6].map(&:strip) == header + end + end + private def writelines path, lines File.unlink path while File.exists? path From 5e3cb3a4eaa557b03d0d18a36f983cc6acec6ad1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 14:19:45 +0900 Subject: [PATCH 339/408] Fix ANSI processor to handle multi-line regions --- src/ansi.go | 10 ++++--- src/ansi_test.go | 71 +++++++++++++++++++++++++++++++++++++++--------- src/core.go | 7 +++-- src/terminal.go | 4 ++- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index 116282c..a80de47 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -36,11 +36,13 @@ func init() { ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") } -func extractColor(str *string) (*string, []ansiOffset) { +func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiState) { var offsets []ansiOffset - var output bytes.Buffer - var state *ansiState + + if state != nil { + offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state}) + } idx := 0 for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) { @@ -76,7 +78,7 @@ func extractColor(str *string) (*string, []ansiOffset) { } } outputStr := output.String() - return &outputStr, offsets + return &outputStr, offsets, state } func interpretCode(ansiCode string, prevState *ansiState) *ansiState { diff --git a/src/ansi_test.go b/src/ansi_test.go index 9f62840..d4d3ca1 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -14,79 +14,89 @@ func TestExtractColor(t *testing.T) { } src := "hello world" + var state *ansiState clean := "\x1b[0m" - check := func(assertion func(ansiOffsets []ansiOffset)) { - output, ansiOffsets := extractColor(&src) + check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) { + output, ansiOffsets, newState := extractColor(&src, state) + state = newState if *output != "hello world" { t.Errorf("Invalid output: {}", output) } fmt.Println(src, ansiOffsets, clean) - assertion(ansiOffsets) + assertion(ansiOffsets, state) } - check(func(offsets []ansiOffset) { + check(func(offsets []ansiOffset, state *ansiState) { if len(offsets) > 0 { t.Fail() } }) + state = nil src = "\x1b[0mhello world" - check(func(offsets []ansiOffset) { + check(func(offsets []ansiOffset, state *ansiState) { if len(offsets) > 0 { t.Fail() } }) + state = nil src = "\x1b[1mhello world" - check(func(offsets []ansiOffset) { + check(func(offsets []ansiOffset, state *ansiState) { if len(offsets) != 1 { t.Fail() } assert(offsets[0], 0, 11, -1, -1, true) }) + state = nil src = "\x1b[1mhello \x1b[mworld" - check(func(offsets []ansiOffset) { + check(func(offsets []ansiOffset, state *ansiState) { if len(offsets) != 1 { t.Fail() } assert(offsets[0], 0, 6, -1, -1, true) }) + state = nil src = "\x1b[1mhello \x1b[Kworld" - check(func(offsets []ansiOffset) { + check(func(offsets []ansiOffset, state *ansiState) { if len(offsets) != 1 { t.Fail() } assert(offsets[0], 0, 11, -1, -1, true) }) + state = nil src = "hello \x1b[34;45;1mworld" - check(func(offsets []ansiOffset) { + check(func(offsets []ansiOffset, state *ansiState) { if len(offsets) != 1 { t.Fail() } assert(offsets[0], 6, 11, 4, 5, true) }) + state = nil src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld" - check(func(offsets []ansiOffset) { + check(func(offsets []ansiOffset, state *ansiState) { if len(offsets) != 1 { t.Fail() } assert(offsets[0], 6, 11, 4, 5, true) }) + state = nil src = "hello \x1b[34;45;1mwor\x1b[0mld" - check(func(offsets []ansiOffset) { + check(func(offsets []ansiOffset, state *ansiState) { if len(offsets) != 1 { t.Fail() } assert(offsets[0], 6, 9, 4, 5, true) }) + state = nil src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md" - check(func(offsets []ansiOffset) { + check(func(offsets []ansiOffset, state *ansiState) { if len(offsets) != 3 { t.Fail() } @@ -96,12 +106,47 @@ func TestExtractColor(t *testing.T) { }) // {38,48};5;{38,48} + state = nil src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md" - check(func(offsets []ansiOffset) { + check(func(offsets []ansiOffset, state *ansiState) { if len(offsets) != 2 { t.Fail() } assert(offsets[0], 6, 9, 38, 48, true) assert(offsets[1], 9, 10, 48, 38, true) }) + + src = "hello \x1b[32;1mworld" + check(func(offsets []ansiOffset, state *ansiState) { + if len(offsets) != 1 { + t.Fail() + } + if state.fg != 2 || state.bg != -1 || !state.bold { + t.Fail() + } + assert(offsets[0], 6, 11, 2, -1, true) + }) + + src = "hello world" + check(func(offsets []ansiOffset, state *ansiState) { + if len(offsets) != 1 { + t.Fail() + } + if state.fg != 2 || state.bg != -1 || !state.bold { + t.Fail() + } + assert(offsets[0], 0, 11, 2, -1, true) + }) + + src = "hello \x1b[0;38;5;200;48;5;100mworld" + check(func(offsets []ansiOffset, state *ansiState) { + if len(offsets) != 2 { + t.Fail() + } + if state.fg != 200 || state.bg != 100 || state.bold { + t.Fail() + } + assert(offsets[0], 0, 6, 2, -1, true) + assert(offsets[1], 6, 11, 200, 100, false) + }) } diff --git a/src/core.go b/src/core.go index e38908a..7a0f119 100644 --- a/src/core.go +++ b/src/core.go @@ -69,14 +69,17 @@ func Run(opts *Options) { } if opts.Ansi { if opts.Theme != nil { + var state *ansiState ansiProcessor = func(data *string) (*string, []ansiOffset) { - return extractColor(data) + trimmed, offsets, newState := extractColor(data, state) + state = newState + return trimmed, offsets } } else { // When color is disabled but ansi option is given, // we simply strip out ANSI codes from the input ansiProcessor = func(data *string) (*string, []ansiOffset) { - trimmed, _ := extractColor(data) + trimmed, _, _ := extractColor(data, nil) return trimmed, nil } } diff --git a/src/terminal.go b/src/terminal.go index 52c36cf..a5ac33c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -378,6 +378,7 @@ func (t *Terminal) printHeader() { return } max := C.MaxY() + var state *ansiState for idx, lineStr := range t.header { if !t.reverse { idx = len(t.header) - idx - 1 @@ -389,7 +390,8 @@ func (t *Terminal) printHeader() { if line >= max { break } - trimmed, colors := extractColor(&lineStr) + trimmed, colors, newState := extractColor(&lineStr, state) + state = newState item := &Item{ text: trimmed, index: 0, From bbaa3ab8bdb4e039d42477f1dcd8640d1211d800 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 14:19:55 +0900 Subject: [PATCH 340/408] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50139d9..84b6f5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ CHANGELOG ========= +0.10.1 +------ + +### New features + +- Added options for sticky header + - `--header-file` + - `--header-lines` + +### Minor improvements/fixes + +- Fixed to allow binding colon and comma keys +- Fixed ANSI processor to handle color regions spanning multiple lines + 0.10.0 ------ From 928fccc15baf50aafe9ecb50303089361375fb89 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 21:22:59 +0900 Subject: [PATCH 341/408] Fix header not shown when the lines go beyond the screen limit --- src/terminal.go | 2 +- test/test_go.rb | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index a5ac33c..c5c9e91 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -388,7 +388,7 @@ func (t *Terminal) printHeader() { line -= 1 } if line >= max { - break + continue } trimmed, colors, newState := extractColor(&lineStr, state) state = newState diff --git a/test/test_go.rb b/test/test_go.rb index 7effb5f..dd113d1 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -680,7 +680,10 @@ class TestGoFZF < TestBase def test_header_lines_overflow tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter - tmux.until { |lines| lines[-2].include?('0/0') } + tmux.until do |lines| + lines[-2].include?('0/0') && + lines[-3].include?(' 1') + end tmux.send_keys :Enter assert_equal '', readonce.chomp end From 4e0e03403e3ce1d6c0d58c02944e167a8448c792 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 21:24:02 +0900 Subject: [PATCH 342/408] Fix --header-lines unaffected by --with-nth --- man/man1/fzf.1 | 8 ++++++-- src/core.go | 6 +++--- test/test_go.rb | 14 +++++++++++++- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index c42fe66..3dc7177 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -260,10 +260,14 @@ automatically truncated when the number of the lines exceeds the value. .TP .BI "--header-file=" "FILE" The content of the file will be printed as the sticky header. The file can -span multiple lines and can contain ANSI color codes. +span multiple lines and can contain ANSI color codes. The lines in the file are +displayed from top to bottom regardless of \fB--reverse\fR, and are +not affected by \fB--with-nth\fR. .TP .BI "--header-lines=" "N" -The first N lines of the input are treated as the sticky header. +The first N lines of the input are treated as the sticky header. When +\fB--with-nth\fR is set, the lines are transformed just like the other +lines that follow. .SS Scripting .TP .BI "-q, --query=" "STR" diff --git a/src/core.go b/src/core.go index 7a0f119..f3d0680 100644 --- a/src/core.go +++ b/src/core.go @@ -104,13 +104,13 @@ func Run(opts *Options) { }) } else { chunkList = NewChunkList(func(data *string, index int) *Item { + tokens := Tokenize(data, opts.Delimiter) + trans := Transform(tokens, opts.WithNth) if len(header) < opts.HeaderLines { - header = append(header, *data) + header = append(header, *joinTokens(trans)) eventBox.Set(EvtHeader, header) return nil } - tokens := Tokenize(data, opts.Delimiter) - trans := Transform(tokens, opts.WithNth) item := Item{ text: joinTokens(trans), origText: data, diff --git a/test/test_go.rb b/test/test_go.rb index dd113d1..7c5cd80 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -688,6 +688,18 @@ class TestGoFZF < TestBase assert_equal '', readonce.chomp end + def test_header_lines_with_nth + tmux.send_keys "seq 100 | #{fzf "--header-lines 5 --with-nth 1,1,1,1,1"}", :Enter + tmux.until do |lines| + lines[-2].include?('95/95') && + lines[-3] == ' 11111' && + lines[-7] == ' 55555' && + lines[-8] == '> 66666' + end + tmux.send_keys :Enter + assert_equal '6', readonce.chomp + end + def test_header_file tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__})"}", :Enter header = File.readlines(__FILE__).take(5).map(&:strip) @@ -698,7 +710,7 @@ class TestGoFZF < TestBase end def test_header_file_reverse - tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__}) --reverse"}", :Enter + tmux.send_keys "seq 100 | #{fzf "--header-file=<(head -5 #{__FILE__}) --reverse"}", :Enter header = File.readlines(__FILE__).take(5).map(&:strip) tmux.until do |lines| lines[1].include?('100/100') && From b53f61fc597d97d852d0f980374e80a34f450777 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 22:36:39 +0900 Subject: [PATCH 343/408] Remove cbreak before raw --- src/curses/curses.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index 9a1dc76..51b6813 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -262,7 +262,6 @@ func Init(theme *ColorTheme, black bool, mouse bool) { if mouse { C.mousemask(C.ALL_MOUSE_EVENTS, nil) } - C.cbreak() C.noecho() C.raw() // stty dsusp undef From cc0d5539ba35b480ae3fb590c562a08637b37064 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 22:56:53 +0900 Subject: [PATCH 344/408] Add "eof" action which closes the finder only when input is empty Close #289 --- CHANGELOG.md | 2 ++ man/man1/fzf.1 | 1 + src/options.go | 2 ++ src/terminal.go | 5 +++++ test/test_go.rb | 13 +++++++++++++ 5 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b6f5e..e3136ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ CHANGELOG - Added options for sticky header - `--header-file` - `--header-lines` +- Added `eof` action which closes the finder only when the input is empty + - e.g. `export FZF_DEFAULT_OPTS="--bind esc:eof"` ### Minor improvements/fixes diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 3dc7177..33db7d5 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -193,6 +193,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBdeselect-all\fR \fBdown\fR \fIctrl-j ctrl-n down\fR \fBend-of-line\fR \fIctrl-e end\fR + \fBeof\fR \fBexecute(...)\fR (see below for the details) \fBforward-char\fR \fIctrl-f right\fR \fBforward-word\fR \fIalt-f shift-right\fR diff --git a/src/options.go b/src/options.go index 17df221..ad5f692 100644 --- a/src/options.go +++ b/src/options.go @@ -499,6 +499,8 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b keymap[key] = actDeleteChar case "end-of-line": keymap[key] = actEndOfLine + case "eof": + keymap[key] = actEof case "forward-char": keymap[key] = actForwardChar case "forward-word": diff --git a/src/terminal.go b/src/terminal.go index c5c9e91..6d95d26 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -103,6 +103,7 @@ const ( actClearScreen actDeleteChar actEndOfLine + actEof actForwardChar actForwardWord actKillLine @@ -813,6 +814,10 @@ func (t *Terminal) Loop() { } case actEndOfLine: t.cx = len(t.input) + case actEof: + if len(t.input) == 0 { + req(reqQuit) + } case actForwardChar: if t.cx < len(t.input) { t.cx++ diff --git a/test/test_go.rb b/test/test_go.rb index 7c5cd80..4f247ed 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -718,6 +718,19 @@ class TestGoFZF < TestBase end end + def test_eof + tmux.send_keys "seq 100 | #{fzf "--bind 2:eof"}", :Enter + tmux.until { |lines| lines[-2].include?('100/100') } + tmux.send_keys '123' + tmux.until do |lines| + lines[-1] == '> 13' && lines[-2].include?('1/100') + end + tmux.send_keys :BSpace, :BSpace + tmux.until { |lines| lines[-1] == '>' } + tmux.send_keys 2 + tmux.prepare + end + private def writelines path, lines File.unlink path while File.exists? path From 13e040baee77d75736f9fbb53f269d5ddc97efb9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Jul 2015 00:56:03 +0900 Subject: [PATCH 345/408] Bind CTRL-D to the new delete-char/eof action - CTRL-D - delete-char/eof - DEL - delete-char --- CHANGELOG.md | 1 + man/man1/fzf.1 | 3 ++- src/options.go | 2 ++ src/terminal.go | 7 +++++-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3136ee..3c6ab2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG - `--header-lines` - Added `eof` action which closes the finder only when the input is empty - e.g. `export FZF_DEFAULT_OPTS="--bind esc:eof"` +- Added `delete-char/eof` action to differentiate `CTRL-D` and `DEL` ### Minor improvements/fixes diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 33db7d5..399bfd4 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -189,7 +189,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBbackward-word\fR \fIalt-b shift-left\fR \fBbeginning-of-line\fR \fIctrl-a home\fR \fBclear-screen\fR \fIctrl-l\fR - \fBdelete-char\fR \fIctrl-d del\fR + \fBdelete-char\fR \fIdel\fR + \fBdelete-char/eof\fR \fIctrl-d\fR \fBdeselect-all\fR \fBdown\fR \fIctrl-j ctrl-n down\fR \fBend-of-line\fR \fIctrl-e end\fR diff --git a/src/options.go b/src/options.go index ad5f692..bb6da9e 100644 --- a/src/options.go +++ b/src/options.go @@ -497,6 +497,8 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b keymap[key] = actClearScreen case "delete-char": keymap[key] = actDeleteChar + case "delete-char/eof": + keymap[key] = actDeleteCharEof case "end-of-line": keymap[key] = actEndOfLine case "eof": diff --git a/src/terminal.go b/src/terminal.go index 6d95d26..cbc19ba 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -102,6 +102,7 @@ const ( actBackwardWord actClearScreen actDeleteChar + actDeleteCharEof actEndOfLine actEof actForwardChar @@ -137,7 +138,7 @@ func defaultKeymap() map[int]actionType { keymap[C.CtrlG] = actAbort keymap[C.CtrlQ] = actAbort keymap[C.ESC] = actAbort - keymap[C.CtrlD] = actDeleteChar + keymap[C.CtrlD] = actDeleteCharEof keymap[C.CtrlE] = actEndOfLine keymap[C.CtrlF] = actForwardChar keymap[C.CtrlH] = actBackwardDeleteChar @@ -168,7 +169,7 @@ func defaultKeymap() map[int]actionType { keymap[C.Home] = actBeginningOfLine keymap[C.End] = actEndOfLine - keymap[C.Del] = actDeleteChar // FIXME Del vs. CTRL-D + keymap[C.Del] = actDeleteChar keymap[C.PgUp] = actPageUp keymap[C.PgDn] = actPageDown @@ -809,6 +810,8 @@ func (t *Terminal) Loop() { case actAbort: req(reqQuit) case actDeleteChar: + t.delChar() + case actDeleteCharEof: if !t.delChar() && t.cx == 0 { req(reqQuit) } From 51d84b18695a72cd340ea7c0dee1efb84f5db233 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Jul 2015 00:58:20 +0900 Subject: [PATCH 346/408] [bash] Update fzf option completion --- shell/completion.bash | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 1e03b07..abe3363 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -48,7 +48,9 @@ _fzf_opts_completion() { --sync --cycle --history - --history-size" + --history-size + --header-file + --header-lines" case "${prev}" in --tiebreak) @@ -59,7 +61,7 @@ _fzf_opts_completion() { COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) ) return 0 ;; - --history) + --history|--header-file) COMPREPLY=() return 0 ;; From f9136cffe6d9a57003f3db39a07546b43df134cb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Jul 2015 10:45:01 +0900 Subject: [PATCH 347/408] Update man page --- man/man1/fzf.1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 399bfd4..17aac59 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -261,10 +261,10 @@ Maximum number of entries in the history file (default: 1000). The file is automatically truncated when the number of the lines exceeds the value. .TP .BI "--header-file=" "FILE" -The content of the file will be printed as the sticky header. The file can -span multiple lines and can contain ANSI color codes. The lines in the file are -displayed from top to bottom regardless of \fB--reverse\fR, and are -not affected by \fB--with-nth\fR. +The content of the file will be printed as the sticky header. The lines in the +file are displayed in order from top to bottom regardless of \fB--reverse\fR, +and are not affected by \fB--with-nth\fR. ANSI color codes are processed even +when \fB--ansi\fR is not set. .TP .BI "--header-lines=" "N" The first N lines of the input are treated as the sticky header. When From fdbf3d3fec4121a52a77330bba383c4878547b2a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Jul 2015 21:05:33 +0900 Subject: [PATCH 348/408] Replace eof action with cancel (#289) --- CHANGELOG.md | 5 +++-- man/man1/fzf.1 | 2 +- src/options.go | 4 ++-- src/terminal.go | 8 ++++++-- test/test_go.rb | 14 +++++++------- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c6ab2c..3f41170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,9 @@ CHANGELOG - Added options for sticky header - `--header-file` - `--header-lines` -- Added `eof` action which closes the finder only when the input is empty - - e.g. `export FZF_DEFAULT_OPTS="--bind esc:eof"` +- Added `cancel` action which clears the input or closes the finder when the + input is already empty + - e.g. `export FZF_DEFAULT_OPTS="--bind esc:cancel"` - Added `delete-char/eof` action to differentiate `CTRL-D` and `DEL` ### Minor improvements/fixes diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 17aac59..0c6b375 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -188,13 +188,13 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-word\fR \fIalt-b shift-left\fR \fBbeginning-of-line\fR \fIctrl-a home\fR + \fBcancel\fR \fBclear-screen\fR \fIctrl-l\fR \fBdelete-char\fR \fIdel\fR \fBdelete-char/eof\fR \fIctrl-d\fR \fBdeselect-all\fR \fBdown\fR \fIctrl-j ctrl-n down\fR \fBend-of-line\fR \fIctrl-e end\fR - \fBeof\fR \fBexecute(...)\fR (see below for the details) \fBforward-char\fR \fIctrl-f right\fR \fBforward-word\fR \fIalt-f shift-right\fR diff --git a/src/options.go b/src/options.go index bb6da9e..5ab511d 100644 --- a/src/options.go +++ b/src/options.go @@ -501,8 +501,8 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b keymap[key] = actDeleteCharEof case "end-of-line": keymap[key] = actEndOfLine - case "eof": - keymap[key] = actEof + case "cancel": + keymap[key] = actCancel case "forward-char": keymap[key] = actForwardChar case "forward-word": diff --git a/src/terminal.go b/src/terminal.go index cbc19ba..18b37d5 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -100,11 +100,11 @@ const ( actBackwardChar actBackwardDeleteChar actBackwardWord + actCancel actClearScreen actDeleteChar actDeleteCharEof actEndOfLine - actEof actForwardChar actForwardWord actKillLine @@ -817,9 +817,13 @@ func (t *Terminal) Loop() { } case actEndOfLine: t.cx = len(t.input) - case actEof: + case actCancel: if len(t.input) == 0 { req(reqQuit) + } else { + t.yanked = t.input + t.input = []rune{} + t.cx = 0 } case actForwardChar: if t.cx < len(t.input) { diff --git a/test/test_go.rb b/test/test_go.rb index 4f247ed..ad2150e 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -718,14 +718,14 @@ class TestGoFZF < TestBase end end - def test_eof - tmux.send_keys "seq 100 | #{fzf "--bind 2:eof"}", :Enter - tmux.until { |lines| lines[-2].include?('100/100') } + def test_canel + tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter + tmux.until { |lines| lines[-2].include?('10/10') } tmux.send_keys '123' - tmux.until do |lines| - lines[-1] == '> 13' && lines[-2].include?('1/100') - end - tmux.send_keys :BSpace, :BSpace + tmux.until { |lines| lines[-1] == '> 3' && lines[-2].include?('1/10') } + tmux.send_keys 'C-y', 'C-y' + tmux.until { |lines| lines[-1] == '> 311' } + tmux.send_keys 2 tmux.until { |lines| lines[-1] == '>' } tmux.send_keys 2 tmux.prepare From 2bebddefc018ff28572bdba0581cb1ffabd494e4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 26 Jul 2015 13:39:34 +0900 Subject: [PATCH 349/408] Do not print the entire --help on invalid option --- src/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/options.go b/src/options.go index 5ab511d..983a7d3 100644 --- a/src/options.go +++ b/src/options.go @@ -181,7 +181,7 @@ func help(ok int) { func errorExit(msg string) { os.Stderr.WriteString(msg + "\n") - help(1) + os.Exit(1) } func optString(arg string, prefixes ...string) (bool, string) { From ee0c8a2635f564870be7926ded092939428a8b57 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 26 Jul 2015 23:02:04 +0900 Subject: [PATCH 350/408] Add --margin option Close #299 --- CHANGELOG.md | 1 + man/man1/fzf.1 | 25 +++++++++ shell/completion.bash | 3 +- src/options.go | 64 +++++++++++++++++++++ src/terminal.go | 127 ++++++++++++++++++++++++++++++------------ test/test_go.rb | 12 ++++ 6 files changed, 196 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f41170..5bd791f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG ### New features +- Added `--margin` option - Added options for sticky header - `--header-file` - `--header-lines` diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 0c6b375..1448eb9 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -131,6 +131,31 @@ Use black background .B "--reverse" Reverse orientation .TP +.BI "--margin=" MARGIN +Comma-separated expression for margins around the finder. +.br +.R "" +.br +.RS +.BR TRBL " Same margin for top, right, bottom, and left" +.br +.BR TB,RL " Vertical, horizontal margin" +.br +.BR T,RL,B " Top, horizontal, bottom margin" +.br +.BR T,R,B,L " Top, right, bottom, left margin" +.br +.R "" +.br +Each part can be given in absolute number or in percentage relative to the +terminal size with \fB%\fR suffix. +.br +.R "" +.br +e.g. \fBfzf --margin 10%\fR + \fBfzf --margin 1,5%\fR +.RE +.TP .B "--cycle" Enable cyclic scroll .TP diff --git a/shell/completion.bash b/shell/completion.bash index abe3363..63de546 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -50,7 +50,8 @@ _fzf_opts_completion() { --history --history-size --header-file - --header-lines" + --header-lines + --margin" case "${prev}" in --tiebreak) diff --git a/src/options.go b/src/options.go index 983a7d3..b2360a1 100644 --- a/src/options.go +++ b/src/options.go @@ -38,6 +38,7 @@ const usage = `usage: fzf [options] --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --black Use black background --reverse Reverse orientation + --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --cycle Enable cyclic scroll --no-hscroll Disable horizontal scroll --inline-info Display finder info inline with the query @@ -93,6 +94,10 @@ const ( byIndex ) +func defaultMargin() [4]string { + return [4]string{"0", "0", "0", "0"} +} + // Options stores the values of command-line options type Options struct { Mode Mode @@ -127,6 +132,7 @@ type Options struct { History *History Header []string HeaderLines int + Margin [4]string Version bool } @@ -171,6 +177,7 @@ func defaultOptions() *Options { History: nil, Header: make([]string, 0), HeaderLines: 0, + Margin: defaultMargin(), Version: false} } @@ -218,6 +225,14 @@ func atoi(str string) int { return num } +func atof(str string) float64 { + num, err := strconv.ParseFloat(str, 64) + if err != nil { + errorExit("not a valid number: " + str) + } + return num +} + func nextInt(args []string, i *int, message string) int { if len(args) > *i+1 { *i++ @@ -592,6 +607,48 @@ func readHeaderFile(filename string) []string { return strings.Split(strings.TrimSuffix(string(content), "\n"), "\n") } +func parseMargin(margin string) [4]string { + margins := strings.Split(margin, ",") + checked := func(str string) string { + if strings.HasSuffix(str, "%") { + val := atof(str[:len(str)-1]) + if val < 0 { + errorExit("margin must be non-negative") + } + if val > 100 { + errorExit("margin too large") + } + } else { + val := atoi(str) + if val < 0 { + errorExit("margin must be non-negative") + } + } + return str + } + switch len(margins) { + case 1: + m := checked(margins[0]) + return [4]string{m, m, m, m} + case 2: + tb := checked(margins[0]) + rl := checked(margins[1]) + return [4]string{tb, rl, tb, rl} + case 3: + t := checked(margins[0]) + rl := checked(margins[1]) + b := checked(margins[2]) + return [4]string{t, rl, b, rl} + case 4: + return [4]string{ + checked(margins[0]), checked(margins[1]), + checked(margins[2]), checked(margins[3])} + default: + errorExit("invalid margin: " + margin) + } + return defaultMargin() +} + func parseOptions(opts *Options, allArgs []string) { keymap := make(map[int]actionType) var historyMax int @@ -743,6 +800,11 @@ func parseOptions(opts *Options, allArgs []string) { opts.Header = []string{} opts.HeaderLines = atoi( nextString(allArgs, &i, "number of header lines required")) + case "--no-margin": + opts.Margin = defaultMargin() + case "--margin": + opts.Margin = parseMargin( + nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) case "--version": opts.Version = true default: @@ -782,6 +844,8 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--header-lines="); match { opts.Header = []string{} opts.HeaderLines = atoi(value) + } else if match, value := optString(arg, "--margin="); match { + opts.Margin = parseMargin(value) } else { errorExit("unknown option: " + arg) } diff --git a/src/terminal.go b/src/terminal.go index 18b37d5..070d0a9 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -8,6 +8,7 @@ import ( "os/signal" "regexp" "sort" + "strconv" "strings" "sync" "syscall" @@ -41,6 +42,8 @@ type Terminal struct { history *History cycle bool header []string + margin [4]string + marginInt [4]int count int progress int reading bool @@ -200,6 +203,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { pressed: "", printQuery: opts.PrintQuery, history: opts.History, + margin: opts.Margin, + marginInt: [4]int{0, 0, 0, 0}, cycle: opts.Cycle, header: opts.Header, reading: true, @@ -317,10 +322,50 @@ func displayWidth(runes []rune) int { return l } +const minWidth = 16 +const minHeight = 4 + +func (t *Terminal) calculateMargins() { + screenWidth := C.MaxX() + screenHeight := C.MaxY() + for idx, str := range t.margin { + if str == "0" { + t.marginInt[idx] = 0 + } else if strings.HasSuffix(str, "%") { + num, _ := strconv.ParseFloat(str[:len(str)-1], 64) + var val float64 + if idx%2 == 0 { + val = float64(screenHeight) + } else { + val = float64(screenWidth) + } + t.marginInt[idx] = int(val * num * 0.01) + } else { + num, _ := strconv.Atoi(str) + t.marginInt[idx] = num + } + } + adjust := func(idx1 int, idx2 int, max int, min int) { + if max >= min { + margin := t.marginInt[idx1] + t.marginInt[idx2] + if max-margin < min { + desired := max - min + t.marginInt[idx1] = desired * t.marginInt[idx1] / margin + t.marginInt[idx2] = desired * t.marginInt[idx2] / margin + } + } + } + adjust(1, 3, screenWidth, minWidth) + adjust(0, 2, screenHeight, minHeight) +} + func (t *Terminal) move(y int, x int, clear bool) { + x += t.marginInt[3] maxy := C.MaxY() if !t.reverse { - y = maxy - y - 1 + y = maxy - y - 1 - t.marginInt[2] + } else { + y += t.marginInt[0] } if clear { @@ -375,11 +420,15 @@ func (t *Terminal) printInfo() { C.CPrint(C.ColInfo, false, output) } +func (t *Terminal) maxHeight() int { + return C.MaxY() - t.marginInt[0] - t.marginInt[2] +} + func (t *Terminal) printHeader() { if len(t.header) == 0 { return } - max := C.MaxY() + max := t.maxHeight() var state *ansiState for idx, lineStr := range t.header { if !t.reverse { @@ -490,7 +539,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c // Overflow text := []rune(*item.text) offsets := item.colorOffsets(col2, bold, current) - maxWidth := C.MaxX() - 3 + maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3] fullWidth := displayWidth(text) if fullWidth > maxWidth { if t.hscroll { @@ -573,6 +622,7 @@ func processTabs(runes []rune, prefixWidth int) (string, int) { } func (t *Terminal) printAll() { + t.calculateMargins() t.printList() t.printPrompt() t.printInfo() @@ -652,6 +702,7 @@ func (t *Terminal) Loop() { { // Late initialization t.mutex.Lock() t.initFunc() + t.calculateMargins() t.printPrompt() t.placeCursor() C.Refresh() @@ -942,40 +993,46 @@ func (t *Terminal) Loop() { } case actMouse: me := event.MouseEvent - mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y - if !t.reverse { - my = C.MaxY() - my - 1 - } - min := 2 + len(t.header) - if t.inlineInfo { - min -= 1 - } - if me.S != 0 { - // Scroll - if t.merger.Length() > 0 { - if t.multi && me.Mod { - toggle() - } - t.vmove(me.S) - req(reqList) + mx, my := me.X, me.Y + if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] && + my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] { + mx -= t.marginInt[3] + my -= t.marginInt[0] + mx = util.Constrain(mx-len(t.prompt), 0, len(t.input)) + if !t.reverse { + my = t.maxHeight() - my - 1 } - } else if me.Double { - // Double-click - if my >= min { - if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { - req(reqClose) - } + min := 2 + len(t.header) + if t.inlineInfo { + min -= 1 } - } else if me.Down { - if my == 0 && mx >= 0 { - // Prompt - t.cx = mx - } else if my >= min { - // List - if t.vset(t.offset+my-min) && t.multi && me.Mod { - toggle() + if me.S != 0 { + // Scroll + if t.merger.Length() > 0 { + if t.multi && me.Mod { + toggle() + } + t.vmove(me.S) + req(reqList) + } + } else if me.Double { + // Double-click + if my >= min { + if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { + req(reqClose) + } + } + } else if me.Down { + if my == 0 && mx >= 0 { + // Prompt + t.cx = mx + } else if my >= min { + // List + if t.vset(t.offset+my-min) && t.multi && me.Mod { + toggle() + } + req(reqList) } - req(reqList) } } } @@ -1040,7 +1097,7 @@ func (t *Terminal) vset(o int) bool { } func (t *Terminal) maxItems() int { - max := C.MaxY() - 2 - len(t.header) + max := t.maxHeight() - 2 - len(t.header) if t.inlineInfo { max += 1 } diff --git a/test/test_go.rb b/test/test_go.rb index ad2150e..f702efc 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -731,6 +731,18 @@ class TestGoFZF < TestBase tmux.prepare end + def test_margin + tmux.send_keys "yes | head -1000 | #{fzf "--margin 5,3"}", :Enter + tmux.until { |lines| lines[4] == '' && lines[5] == ' y' } + tmux.send_keys :Enter + end + + def test_margin_reverse + tmux.send_keys "seq 1000 | #{fzf "--margin 7,5 --reverse"}", :Enter + tmux.until { |lines| lines[1 + 7] == ' 1000/1000' } + tmux.send_keys :Enter + end + private def writelines path, lines File.unlink path while File.exists? path From 6166e2dd805b4647dbf20facab71ba0745c1ec44 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 26 Jul 2015 23:57:26 +0900 Subject: [PATCH 351/408] 0.10.1 --- install | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install b/install index 0527772..7b25155 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.10.0 +version=0.10.1 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 1448eb9..3a334cd 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "June 2015" "fzf 0.10.0" "fzf - a command-line fuzzy finder" +.TH fzf 1 "July 2015" "fzf 0.10.1" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 2692907..cc63af3 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - Version = "0.10.0" + Version = "0.10.1" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 7814371a9a826a12476b446fcce0100932f96dbd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 27 Jul 2015 00:03:14 +0900 Subject: [PATCH 352/408] Revert "0.10.1" This reverts commit 6166e2dd805b4647dbf20facab71ba0745c1ec44. --- install | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install b/install index 7b25155..0527772 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.10.1 +version=0.10.0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 3a334cd..1448eb9 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "July 2015" "fzf 0.10.1" "fzf - a command-line fuzzy finder" +.TH fzf 1 "June 2015" "fzf 0.10.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index cc63af3..2692907 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - Version = "0.10.1" + Version = "0.10.0" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 7231acd44297850730fed1f6069b340e1885f352 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 27 Jul 2015 00:06:44 +0900 Subject: [PATCH 353/408] Fix mouse scroll when --margin is set --- src/terminal.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 070d0a9..9466170 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -994,7 +994,16 @@ func (t *Terminal) Loop() { case actMouse: me := event.MouseEvent mx, my := me.X, me.Y - if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] && + if me.S != 0 { + // Scroll + if t.merger.Length() > 0 { + if t.multi && me.Mod { + toggle() + } + t.vmove(me.S) + req(reqList) + } + } else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] && my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] { mx -= t.marginInt[3] my -= t.marginInt[0] @@ -1006,16 +1015,7 @@ func (t *Terminal) Loop() { if t.inlineInfo { min -= 1 } - if me.S != 0 { - // Scroll - if t.merger.Length() > 0 { - if t.multi && me.Mod { - toggle() - } - t.vmove(me.S) - req(reqList) - } - } else if me.Double { + if me.Double { // Double-click if my >= min { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { From 909ea1a698e9021fbae3366b1e71ba12f0dd7270 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 26 Jul 2015 23:57:26 +0900 Subject: [PATCH 354/408] 0.10.1 --- install | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install b/install index 0527772..7b25155 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -version=0.10.0 +version=0.10.1 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 1448eb9..3a334cd 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "June 2015" "fzf 0.10.0" "fzf - a command-line fuzzy finder" +.TH fzf 1 "July 2015" "fzf 0.10.1" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 2692907..cc63af3 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - Version = "0.10.0" + Version = "0.10.1" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 9f953fc9440a6df9dcfc177555e0b4dd7444fd0a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 28 Jul 2015 00:22:04 +0900 Subject: [PATCH 355/408] Do not use tmux pane if the current pane is zoomed Close #303 --- bin/fzf-tmux | 2 +- plugin/fzf.vim | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index c723288..e0fe77d 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -82,7 +82,7 @@ while [ $# -gt 0 ]; do shift done -if [ -z "$TMUX_PANE" ]; then +if [ -z "$TMUX_PANE" ] || tmux list-panes -F '#F' | grep -q Z; then fzf "${args[@]}" exit $? fi diff --git a/plugin/fzf.vim b/plugin/fzf.vim index a149e79..1a245d9 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -64,7 +64,8 @@ function! s:tmux_enabled() endif let s:tmux = 0 - if exists('$TMUX') && executable(s:fzf_tmux) + let panes = system('tmux list-panes -F "#F"') + if exists('$TMUX') && executable(s:fzf_tmux) && panes !~# 'Z' let output = system('tmux -V') let s:tmux = !v:shell_error && output >= 'tmux 1.7' endif From fb1b026d3daaaa9b64d8f49ae6fbbf6727ac33c0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 28 Jul 2015 00:30:01 +0900 Subject: [PATCH 356/408] Always check if the pane is zoomed Close #303 --- plugin/fzf.vim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 1a245d9..c4deb9e 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -59,17 +59,17 @@ function! s:tmux_enabled() return 0 endif + let not_zoomed = system('tmux list-panes -F "#F"') !~# 'Z' if exists('s:tmux') - return s:tmux + return s:tmux && not_zoomed endif let s:tmux = 0 - let panes = system('tmux list-panes -F "#F"') - if exists('$TMUX') && executable(s:fzf_tmux) && panes !~# 'Z' + if exists('$TMUX') && executable(s:fzf_tmux) let output = system('tmux -V') let s:tmux = !v:shell_error && output >= 'tmux 1.7' endif - return s:tmux + return s:tmux && not_zoomed endfunction function! s:shellesc(arg) From fcae99f09b9ed700465ac06b375c376cc2d6dde8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 28 Jul 2015 00:48:29 +0900 Subject: [PATCH 357/408] No need to "tmux list-panes" when obviously not on tmux (#303) --- plugin/fzf.vim | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index c4deb9e..c9eb6d1 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -54,14 +54,17 @@ function! s:fzf_exec() return s:exec endfunction +function! s:tmux_not_zoomed() + return system('tmux list-panes -F "#F"') !~# 'Z' +endfunction + function! s:tmux_enabled() if has('gui_running') return 0 endif - let not_zoomed = system('tmux list-panes -F "#F"') !~# 'Z' if exists('s:tmux') - return s:tmux && not_zoomed + return s:tmux && s:tmux_not_zoomed() endif let s:tmux = 0 @@ -69,7 +72,7 @@ function! s:tmux_enabled() let output = system('tmux -V') let s:tmux = !v:shell_error && output >= 'tmux 1.7' endif - return s:tmux && not_zoomed + return s:tmux && s:tmux_not_zoomed() endfunction function! s:shellesc(arg) From dce6fe6f2ddacf0cef675342d71b6c740fdbc6b2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 28 Jul 2015 14:17:04 +0900 Subject: [PATCH 358/408] [fzf-tmux] Ensure that the same $TERM value is used in split Fix #305. ncurses can crash on invalid $TERM. fzf-tmux uses bash on a new pane so we have to make sure that the $TERM is consistent with that of the hosting shell. --- bin/fzf-tmux | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index e0fe77d..abed1ee 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -107,7 +107,7 @@ fail() { fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" [ -x "$fzf" ] || fail "fzf executable not found" -envs="env " +envs="env TERM=$TERM " [ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" From 02bd2d2adf73feddd57bdac6c5fbaee56edaf53e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 28 Jul 2015 14:35:46 +0900 Subject: [PATCH 359/408] Do not proceed if $TERM is invalid Related #305 --- src/curses/curses.go | 5 +++++ test/test_go.rb | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/curses/curses.go b/src/curses/curses.go index 51b6813..3de8e98 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -8,6 +8,7 @@ package curses import "C" import ( + "fmt" "os" "os/signal" "syscall" @@ -258,6 +259,10 @@ func Init(theme *ColorTheme, black bool, mouse bool) { C.setlocale(C.LC_ALL, C.CString("")) _screen = C.newterm(nil, C.stderr, C.stdin) + if _screen == nil { + fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) + os.Exit(1) + } C.set_term(_screen) if mouse { C.mousemask(C.ALL_MOUSE_EVENTS, nil) diff --git a/test/test_go.rb b/test/test_go.rb index f702efc..75fc01c 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -743,6 +743,11 @@ class TestGoFZF < TestBase tmux.send_keys :Enter end + def test_invalid_term + tmux.send_keys "TERM=xxx fzf", :Enter + tmux.until { |lines| lines.any? { |l| l.include? 'Invalid $TERM: xxx' } } + end + private def writelines path, lines File.unlink path while File.exists? path From 0b4542fcdf52b51e026db230a0ec6c06e5eff00d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 29 Jul 2015 17:55:58 +0900 Subject: [PATCH 360/408] [vim] Temporarily disable &autochdir when opening files (#306) --- plugin/fzf.vim | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index c9eb6d1..d81784d 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -368,9 +368,15 @@ function! s:cmd_callback(lines) abort endif let key = remove(a:lines, 0) let cmd = get(s:action, key, 'e') - for item in a:lines - execute cmd s:escape(item) - endfor + try + let autochdir = &autochdir + set noautochdir + for item in a:lines + execute cmd s:escape(item) + endfor + finally + let &autochdir = autochdir + endtry endfunction function! s:cmd(bang, ...) abort From 5e90f0a57bf21393a12ffc05e022760a71098d92 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 1 Aug 2015 21:48:35 +0900 Subject: [PATCH 361/408] Fix default command so that it doesn't fail on dash-prefixed files Close #310 --- src/constants.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.go b/src/constants.go index cc63af3..a238295 100644 --- a/src/constants.go +++ b/src/constants.go @@ -15,7 +15,7 @@ const ( coordinatorDelayStep time.Duration = 10 * time.Millisecond // Reader - defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` + defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` // Terminal initialDelay = 100 * time.Millisecond From dea60b11bca22f4bf886ae7026bac44521038010 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 1 Aug 2015 23:13:24 +0900 Subject: [PATCH 362/408] Only consider the lengths of the relevant parts when --nth is set --- src/item.go | 11 ++++++++++- test/test_go.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/item.go b/src/item.go index 1eeb180..68f5297 100644 --- a/src/item.go +++ b/src/item.go @@ -63,7 +63,16 @@ func (i *Item) Rank(cache bool) Rank { var tiebreak uint16 switch rankTiebreak { case byLength: - tiebreak = uint16(len(*i.text)) + // It is guaranteed that .transformed in not null in normal execution + if i.transformed != nil { + lenSum := 0 + for _, token := range *i.transformed { + lenSum += len(*token.text) + } + tiebreak = uint16(lenSum) + } else { + tiebreak = uint16(len(*i.text)) + } case byBegin: // We can't just look at i.offsets[0][0] because it can be an inverse term tiebreak = uint16(minBegin) diff --git a/test/test_go.rb b/test/test_go.rb index 75fc01c..c21c94e 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -501,6 +501,32 @@ class TestGoFZF < TestBase assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/) end + def test_tiebreak_length_with_nth + input = %w[ + 1:hell + 123:hello + 12345:he + 1234567:h + ] + writelines tempname, input + + output = %w[ + 1:hell + 12345:he + 123:hello + 1234567:h + ] + assert_equal output, `cat #{tempname} | #{FZF} -fh`.split($/) + + output = %w[ + 1234567:h + 12345:he + 1:hell + 123:hello + ] + assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/) + end + def test_invalid_cache tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter tmux.until { |lines| lines[-2].include? '2/3' } From 634670e3ea51a2fa1498a3de0c074b819828e2d8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 2 Aug 2015 13:06:15 +0900 Subject: [PATCH 363/408] Lint --- src/constants.go | 2 +- src/core.go | 2 +- src/history.go | 2 ++ src/matcher.go | 2 +- src/merger.go | 2 +- src/options.go | 2 +- src/terminal.go | 14 +++++++------- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/constants.go b/src/constants.go index a238295..91c863a 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - Version = "0.10.1" + version = "0.10.1" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond diff --git a/src/core.go b/src/core.go index f3d0680..7e40bd5 100644 --- a/src/core.go +++ b/src/core.go @@ -55,7 +55,7 @@ func Run(opts *Options) { rankTiebreak = opts.Tiebreak if opts.Version { - fmt.Println(Version) + fmt.Println(version) os.Exit(0) } diff --git a/src/history.go b/src/history.go index 66159eb..4aa87fc 100644 --- a/src/history.go +++ b/src/history.go @@ -7,6 +7,7 @@ import ( "strings" ) +// History struct represents input history type History struct { path string lines []string @@ -15,6 +16,7 @@ type History struct { cursor int } +// NewHistory returns the pointer to a new History struct func NewHistory(path string, maxSize int) (*History, error) { fmtError := func(e error) error { if os.IsPermission(e) { diff --git a/src/matcher.go b/src/matcher.go index d01ed23..3ea2fbe 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -96,7 +96,7 @@ func (m *Matcher) Loop() { } if !cancelled { - if merger.Cacheable() { + if merger.cacheable() { m.mergerCache[patternString] = merger } merger.final = request.final diff --git a/src/merger.go b/src/merger.go index 4c7966a..cce8a94 100644 --- a/src/merger.go +++ b/src/merger.go @@ -82,7 +82,7 @@ func (mg *Merger) Get(idx int) *Item { panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) } -func (mg *Merger) Cacheable() bool { +func (mg *Merger) cacheable() bool { return mg.count < mergerCacheMax } diff --git a/src/options.go b/src/options.go index b2360a1..6a0d197 100644 --- a/src/options.go +++ b/src/options.go @@ -513,7 +513,7 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b case "delete-char": keymap[key] = actDeleteChar case "delete-char/eof": - keymap[key] = actDeleteCharEof + keymap[key] = actDeleteCharEOF case "end-of-line": keymap[key] = actEndOfLine case "cancel": diff --git a/src/terminal.go b/src/terminal.go index 9466170..6c3b147 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -106,7 +106,7 @@ const ( actCancel actClearScreen actDeleteChar - actDeleteCharEof + actDeleteCharEOF actEndOfLine actForwardChar actForwardWord @@ -141,7 +141,7 @@ func defaultKeymap() map[int]actionType { keymap[C.CtrlG] = actAbort keymap[C.CtrlQ] = actAbort keymap[C.ESC] = actAbort - keymap[C.CtrlD] = actDeleteCharEof + keymap[C.CtrlD] = actDeleteCharEOF keymap[C.CtrlE] = actEndOfLine keymap[C.CtrlF] = actForwardChar keymap[C.CtrlH] = actBackwardDeleteChar @@ -436,7 +436,7 @@ func (t *Terminal) printHeader() { } line := idx + 2 if t.inlineInfo { - line -= 1 + line-- } if line >= max { continue @@ -462,7 +462,7 @@ func (t *Terminal) printList() { for i := 0; i < maxy; i++ { line := i + 2 + len(t.header) if t.inlineInfo { - line -= 1 + line-- } t.move(line, 0, true) if i < count { @@ -862,7 +862,7 @@ func (t *Terminal) Loop() { req(reqQuit) case actDeleteChar: t.delChar() - case actDeleteCharEof: + case actDeleteCharEOF: if !t.delChar() && t.cx == 0 { req(reqQuit) } @@ -1013,7 +1013,7 @@ func (t *Terminal) Loop() { } min := 2 + len(t.header) if t.inlineInfo { - min -= 1 + min-- } if me.Double { // Double-click @@ -1099,7 +1099,7 @@ func (t *Terminal) vset(o int) bool { func (t *Terminal) maxItems() int { max := t.maxHeight() - 2 - len(t.header) if t.inlineInfo { - max += 1 + max++ } return util.Max(max, 0) } From 0ea66329b84cc6e4f8ff61ee99c00bb238070247 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 2 Aug 2015 14:00:18 +0900 Subject: [PATCH 364/408] Performance tuning - eager rune array conversion > wc -l /tmp/list2 2594098 /tmp/list2 > time cat /tmp/list2 | fzf-0.10.1-darwin_amd64 -fqwerty > /dev/null real 0m5.418s user 0m10.990s sys 0m1.302s > time cat /tmp/list2 | fzf-head -fqwerty > /dev/null real 0m4.862s user 0m6.619s sys 0m0.982s --- src/algo/algo.go | 26 ++++++++++++------------- src/algo/algo_test.go | 5 ++--- src/ansi.go | 13 ++++++------- src/ansi_test.go | 4 ++-- src/chunklist.go | 8 ++++---- src/chunklist_test.go | 16 +++++++-------- src/core.go | 36 +++++++++++++++++----------------- src/item.go | 20 ++++++++++--------- src/item_test.go | 14 +++++++------- src/merger_test.go | 2 +- src/pattern.go | 18 ++++++++--------- src/pattern_test.go | 23 +++++++++++----------- src/reader.go | 27 +++++++++++++++++++------- src/reader_test.go | 2 +- src/terminal.go | 6 +++--- src/tokenizer.go | 45 +++++++++++++++++++++---------------------- src/tokenizer_test.go | 44 +++++++++++++++++++++--------------------- src/util/util.go | 8 ++++---- 18 files changed, 163 insertions(+), 154 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index c93563a..afc12aa 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -16,7 +16,7 @@ import ( */ // FuzzyMatch performs fuzzy-match -func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { +func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { if len(pattern) == 0 { return 0, 0 } @@ -34,7 +34,7 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { sidx := -1 eidx := -1 - for index, char := range *runes { + for index, char := range runes { // This is considerably faster than blindly applying strings.ToLower to the // whole string if !caseSensitive { @@ -61,7 +61,7 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { if sidx >= 0 && eidx >= 0 { pidx-- for index := eidx - 1; index >= sidx; index-- { - char := (*runes)[index] + char := runes[index] if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -88,12 +88,12 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { // // We might try to implement better algorithms in the future: // http://en.wikipedia.org/wiki/String_searching_algorithm -func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { +func ExactMatchNaive(caseSensitive bool, runes []rune, pattern []rune) (int, int) { if len(pattern) == 0 { return 0, 0 } - numRunes := len(*runes) + numRunes := len(runes) plen := len(pattern) if numRunes < plen { return -1, -1 @@ -101,7 +101,7 @@ func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, in pidx := 0 for index := 0; index < numRunes; index++ { - char := (*runes)[index] + char := runes[index] if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -123,13 +123,13 @@ func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, in } // PrefixMatch performs prefix-match -func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { - if len(*runes) < len(pattern) { +func PrefixMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { + if len(runes) < len(pattern) { return -1, -1 } for index, r := range pattern { - char := (*runes)[index] + char := runes[index] if !caseSensitive { char = unicode.ToLower(char) } @@ -141,7 +141,7 @@ func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { } // SuffixMatch performs suffix-match -func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) { +func SuffixMatch(caseSensitive bool, input []rune, pattern []rune) (int, int) { runes := util.TrimRight(input) trimmedLen := len(runes) diff := trimmedLen - len(pattern) @@ -161,11 +161,11 @@ func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) { return trimmedLen - len(pattern), trimmedLen } -func EqualMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { - if len(*runes) != len(pattern) { +func EqualMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { + if len(runes) != len(pattern) { return -1, -1 } - runesStr := string(*runes) + runesStr := string(runes) if !caseSensitive { runesStr = strings.ToLower(runesStr) } diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index 32056df..db24196 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -5,12 +5,11 @@ import ( "testing" ) -func assertMatch(t *testing.T, fun func(bool, *[]rune, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { +func assertMatch(t *testing.T, fun func(bool, []rune, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { if !caseSensitive { pattern = strings.ToLower(pattern) } - runes := []rune(input) - s, e := fun(caseSensitive, &runes, []rune(pattern)) + s, e := fun(caseSensitive, []rune(input), []rune(pattern)) if s != sidx { t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) } diff --git a/src/ansi.go b/src/ansi.go index a80de47..876229f 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -36,7 +36,7 @@ func init() { ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") } -func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiState) { +func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiState) { var offsets []ansiOffset var output bytes.Buffer @@ -45,9 +45,9 @@ func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiSt } idx := 0 - for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) { - output.WriteString((*str)[idx:offset[0]]) - newState := interpretCode((*str)[offset[0]:offset[1]], state) + for _, offset := range ansiRegex.FindAllStringIndex(str, -1) { + output.WriteString(str[idx:offset[0]]) + newState := interpretCode(str[offset[0]:offset[1]], state) if !newState.equals(state) { if state != nil { @@ -69,7 +69,7 @@ func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiSt idx = offset[1] } - rest := (*str)[idx:] + rest := str[idx:] if len(rest) > 0 { output.WriteString(rest) if state != nil { @@ -77,8 +77,7 @@ func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiSt (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes())) } } - outputStr := output.String() - return &outputStr, offsets, state + return output.String(), offsets, state } func interpretCode(ansiCode string, prevState *ansiState) *ansiState { diff --git a/src/ansi_test.go b/src/ansi_test.go index d4d3ca1..e278fe9 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -17,9 +17,9 @@ func TestExtractColor(t *testing.T) { var state *ansiState clean := "\x1b[0m" check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) { - output, ansiOffsets, newState := extractColor(&src, state) + output, ansiOffsets, newState := extractColor(src, state) state = newState - if *output != "hello world" { + if output != "hello world" { t.Errorf("Invalid output: {}", output) } fmt.Println(src, ansiOffsets, clean) diff --git a/src/chunklist.go b/src/chunklist.go index ee52d32..c20ffd4 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -7,7 +7,7 @@ type Chunk []*Item // >>> []Item // ItemBuilder is a closure type that builds Item object from a pointer to a // string and an integer -type ItemBuilder func(*string, int) *Item +type ItemBuilder func([]rune, int) *Item // ChunkList is a list of Chunks type ChunkList struct { @@ -26,7 +26,7 @@ func NewChunkList(trans ItemBuilder) *ChunkList { trans: trans} } -func (c *Chunk) push(trans ItemBuilder, data *string, index int) bool { +func (c *Chunk) push(trans ItemBuilder, data []rune, index int) bool { item := trans(data, index) if item != nil { *c = append(*c, item) @@ -53,7 +53,7 @@ func CountItems(cs []*Chunk) int { } // Push adds the item to the list -func (cl *ChunkList) Push(data string) bool { +func (cl *ChunkList) Push(data []rune) bool { cl.mutex.Lock() defer cl.mutex.Unlock() @@ -62,7 +62,7 @@ func (cl *ChunkList) Push(data string) bool { cl.chunks = append(cl.chunks, &newChunk) } - if cl.lastChunk().push(cl.trans, &data, cl.count) { + if cl.lastChunk().push(cl.trans, data, cl.count) { cl.count++ return true } diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 2f8ef7e..faaf04f 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -6,7 +6,7 @@ import ( ) func TestChunkList(t *testing.T) { - cl := NewChunkList(func(s *string, i int) *Item { + cl := NewChunkList(func(s []rune, i int) *Item { return &Item{text: s, rank: Rank{0, 0, uint32(i * 2)}} }) @@ -17,8 +17,8 @@ func TestChunkList(t *testing.T) { } // Add some data - cl.Push("hello") - cl.Push("world") + cl.Push([]rune("hello")) + cl.Push([]rune("world")) // Previously created snapshot should remain the same if len(snapshot) > 0 { @@ -36,8 +36,8 @@ func TestChunkList(t *testing.T) { if len(*chunk1) != 2 { t.Error("Snapshot should contain only two items") } - if *(*chunk1)[0].text != "hello" || (*chunk1)[0].rank.index != 0 || - *(*chunk1)[1].text != "world" || (*chunk1)[1].rank.index != 2 { + if string((*chunk1)[0].text) != "hello" || (*chunk1)[0].rank.index != 0 || + string((*chunk1)[1].text) != "world" || (*chunk1)[1].rank.index != 2 { t.Error("Invalid data") } if chunk1.IsFull() { @@ -46,7 +46,7 @@ func TestChunkList(t *testing.T) { // Add more data for i := 0; i < chunkSize*2; i++ { - cl.Push(fmt.Sprintf("item %d", i)) + cl.Push([]rune(fmt.Sprintf("item %d", i))) } // Previous snapshot should remain the same @@ -64,8 +64,8 @@ func TestChunkList(t *testing.T) { t.Error("Unexpected number of items") } - cl.Push("hello") - cl.Push("world") + cl.Push([]rune("hello")) + cl.Push([]rune("world")) lastChunkCount := len(*snapshot[len(snapshot)-1]) if lastChunkCount != 2 { diff --git a/src/core.go b/src/core.go index 7e40bd5..c0596e3 100644 --- a/src/core.go +++ b/src/core.go @@ -63,24 +63,24 @@ func Run(opts *Options) { eventBox := util.NewEventBox() // ANSI code processor - ansiProcessor := func(data *string) (*string, []ansiOffset) { + ansiProcessor := func(runes []rune) ([]rune, []ansiOffset) { // By default, we do nothing - return data, nil + return runes, nil } if opts.Ansi { if opts.Theme != nil { var state *ansiState - ansiProcessor = func(data *string) (*string, []ansiOffset) { - trimmed, offsets, newState := extractColor(data, state) + ansiProcessor = func(runes []rune) ([]rune, []ansiOffset) { + trimmed, offsets, newState := extractColor(string(runes), state) state = newState - return trimmed, offsets + return []rune(trimmed), offsets } } else { // When color is disabled but ansi option is given, // we simply strip out ANSI codes from the input - ansiProcessor = func(data *string) (*string, []ansiOffset) { - trimmed, _, _ := extractColor(data, nil) - return trimmed, nil + ansiProcessor = func(runes []rune) ([]rune, []ansiOffset) { + trimmed, _, _ := extractColor(string(runes), nil) + return []rune(trimmed), nil } } } @@ -89,9 +89,9 @@ func Run(opts *Options) { var chunkList *ChunkList header := make([]string, 0, opts.HeaderLines) if len(opts.WithNth) == 0 { - chunkList = NewChunkList(func(data *string, index int) *Item { + chunkList = NewChunkList(func(data []rune, index int) *Item { if len(header) < opts.HeaderLines { - header = append(header, *data) + header = append(header, string(data)) eventBox.Set(EvtHeader, header) return nil } @@ -103,17 +103,17 @@ func Run(opts *Options) { rank: Rank{0, 0, uint32(index)}} }) } else { - chunkList = NewChunkList(func(data *string, index int) *Item { + chunkList = NewChunkList(func(data []rune, index int) *Item { tokens := Tokenize(data, opts.Delimiter) trans := Transform(tokens, opts.WithNth) if len(header) < opts.HeaderLines { - header = append(header, *joinTokens(trans)) + header = append(header, string(joinTokens(trans))) eventBox.Set(EvtHeader, header) return nil } item := Item{ text: joinTokens(trans), - origText: data, + origText: &data, index: uint32(index), colors: nil, rank: Rank{0, 0, uint32(index)}} @@ -128,8 +128,8 @@ func Run(opts *Options) { // Reader streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync if !streamingFilter { - reader := Reader{func(str string) bool { - return chunkList.Push(str) + reader := Reader{func(data []rune) bool { + return chunkList.Push(data) }, eventBox, opts.ReadZero} go reader.ReadSource() } @@ -151,10 +151,10 @@ func Run(opts *Options) { if streamingFilter { reader := Reader{ - func(str string) bool { - item := chunkList.trans(&str, 0) + func(runes []rune) bool { + item := chunkList.trans(runes, 0) if item != nil && pattern.MatchItem(item) { - fmt.Println(*item.text) + fmt.Println(string(item.text)) } return false }, eventBox, opts.ReadZero} diff --git a/src/item.go b/src/item.go index 68f5297..96f3d23 100644 --- a/src/item.go +++ b/src/item.go @@ -17,9 +17,9 @@ type colorOffset struct { // Item represents each input line type Item struct { - text *string - origText *string - transformed *[]Token + text []rune + origText *[]rune + transformed []Token index uint32 offsets []Offset colors []ansiOffset @@ -66,19 +66,19 @@ func (i *Item) Rank(cache bool) Rank { // It is guaranteed that .transformed in not null in normal execution if i.transformed != nil { lenSum := 0 - for _, token := range *i.transformed { - lenSum += len(*token.text) + for _, token := range i.transformed { + lenSum += len(token.text) } tiebreak = uint16(lenSum) } else { - tiebreak = uint16(len(*i.text)) + tiebreak = uint16(len(i.text)) } case byBegin: // We can't just look at i.offsets[0][0] because it can be an inverse term tiebreak = uint16(minBegin) case byEnd: if prevEnd > 0 { - tiebreak = uint16(1 + len(*i.text) - prevEnd) + tiebreak = uint16(1 + len(i.text) - prevEnd) } else { // Empty offsets due to inverse terms. tiebreak = 1 @@ -100,10 +100,12 @@ func (i *Item) AsString() string { // StringPtr returns the pointer to the original string func (i *Item) StringPtr() *string { + runes := i.text if i.origText != nil { - return i.origText + runes = *i.origText } - return i.text + str := string(runes) + return &str } func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset { diff --git a/src/item_test.go b/src/item_test.go index 2d375e4..5b9232a 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -39,14 +39,14 @@ func TestRankComparison(t *testing.T) { // Match length, string length, index func TestItemRank(t *testing.T) { - strs := []string{"foo", "foobar", "bar", "baz"} - item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}} + strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} + item1 := Item{text: strs[0], index: 1, offsets: []Offset{}} rank1 := item1.Rank(true) if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 { t.Error(item1.Rank(true)) } // Only differ in index - item2 := Item{text: &strs[0], index: 0, offsets: []Offset{}} + item2 := Item{text: strs[0], index: 0, offsets: []Offset{}} items := []*Item{&item1, &item2} sort.Sort(ByRelevance(items)) @@ -62,10 +62,10 @@ func TestItemRank(t *testing.T) { } // Sort by relevance - item3 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item4 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} - item5 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item6 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item3 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item4 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item5 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item6 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} sort.Sort(ByRelevance(items)) if items[0] != &item2 || items[1] != &item1 || diff --git a/src/merger_test.go b/src/merger_test.go index b69d633..b7a2993 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -22,7 +22,7 @@ func randItem() *Item { offsets[idx] = Offset{sidx, eidx} } return &Item{ - text: &str, + text: []rune(str), index: rand.Uint32(), offsets: offsets} } diff --git a/src/pattern.go b/src/pattern.go index ffdf6d8..990450a 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -44,7 +44,7 @@ type Pattern struct { hasInvTerm bool delimiter *regexp.Regexp nth []Range - procFun map[termType]func(bool, *[]rune, []rune) (int, int) + procFun map[termType]func(bool, []rune, []rune) (int, int) } var ( @@ -114,7 +114,7 @@ func BuildPattern(mode Mode, caseMode Case, hasInvTerm: hasInvTerm, nth: nth, delimiter: delimiter, - procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))} + procFun: make(map[termType]func(bool, []rune, []rune) (int, int))} ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termEqual] = algo.EqualMatch @@ -305,27 +305,25 @@ func (p *Pattern) extendedMatch(item *Item) []Offset { return offsets } -func (p *Pattern) prepareInput(item *Item) *[]Token { +func (p *Pattern) prepareInput(item *Item) []Token { if item.transformed != nil { return item.transformed } - var ret *[]Token + var ret []Token if len(p.nth) > 0 { tokens := Tokenize(item.text, p.delimiter) ret = Transform(tokens, p.nth) } else { - runes := []rune(*item.text) - trans := []Token{Token{text: &runes, prefixLength: 0}} - ret = &trans + ret = []Token{Token{text: item.text, prefixLength: 0}} } item.transformed = ret return ret } -func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int), - tokens *[]Token, caseSensitive bool, pattern []rune) (int, int) { - for _, part := range *tokens { +func (p *Pattern) iter(pfun func(bool, []rune, []rune) (int, int), + tokens []Token, caseSensitive bool, pattern []rune) (int, int) { + for _, part := range tokens { prefixLength := part.prefixLength if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 { return sidx + prefixLength, eidx + prefixLength diff --git a/src/pattern_test.go b/src/pattern_test.go index fe6561c..8134cdc 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -1,6 +1,7 @@ package fzf import ( + "reflect" "testing" "github.com/junegunn/fzf/src/algo" @@ -59,8 +60,8 @@ func TestExact(t *testing.T) { clearPatternCache() pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("'abc")) - runes := []rune("aabbcc abc") - sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &runes, pattern.terms[0].text) + sidx, eidx := algo.ExactMatchNaive( + pattern.caseSensitive, []rune("aabbcc abc"), pattern.terms[0].text) if sidx != 7 || eidx != 10 { t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) } @@ -72,8 +73,8 @@ func TestEqual(t *testing.T) { pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { - runes := []rune(str) - sidx, eidx := algo.EqualMatch(pattern.caseSensitive, &runes, pattern.terms[0].text) + sidx, eidx := algo.EqualMatch( + pattern.caseSensitive, []rune(str), pattern.terms[0].text) if sidx != sidxExpected || eidx != eidxExpected { t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) } @@ -108,25 +109,23 @@ func TestCaseSensitivity(t *testing.T) { } func TestOrigTextAndTransformed(t *testing.T) { - strptr := func(str string) *string { - return &str - } pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("jg")) - tokens := Tokenize(strptr("junegunn"), nil) + tokens := Tokenize([]rune("junegunn"), nil) trans := Transform(tokens, []Range{Range{1, 1}}) + origRunes := []rune("junegunn.choi") for _, mode := range []Mode{ModeFuzzy, ModeExtended} { chunk := Chunk{ &Item{ - text: strptr("junegunn"), - origText: strptr("junegunn.choi"), + text: []rune("junegunn"), + origText: &origRunes, transformed: trans}, } pattern.mode = mode matches := pattern.matchChunk(&chunk) - if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" || + if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || - matches[0].transformed != trans { + !reflect.DeepEqual(matches[0].transformed, trans) { t.Error("Invalid match result", matches) } } diff --git a/src/reader.go b/src/reader.go index aab8b02..d979eb6 100644 --- a/src/reader.go +++ b/src/reader.go @@ -5,13 +5,14 @@ import ( "io" "os" "os/exec" + "unicode/utf8" "github.com/junegunn/fzf/src/util" ) // Reader reads from command or standard input type Reader struct { - pusher func(string) bool + pusher func([]rune) bool eventBox *util.EventBox delimNil bool } @@ -37,13 +38,25 @@ func (r *Reader) feed(src io.Reader) { } reader := bufio.NewReader(src) for { - line, err := reader.ReadString(delim) - if line != "" { - // "ReadString returns err != nil if and only if the returned data does not end in delim." - if err == nil { - line = line[:len(line)-1] + // ReadBytes returns err != nil if and only if the returned data does not + // end in delim. + bytea, err := reader.ReadBytes(delim) + if len(bytea) > 0 { + runes := make([]rune, 0, len(bytea)) + for i := 0; i < len(bytea); { + if bytea[i] < utf8.RuneSelf { + runes = append(runes, rune(bytea[i])) + i++ + } else { + r, sz := utf8.DecodeRune(bytea[i:]) + i += sz + runes = append(runes, r) + } } - if r.pusher(line) { + if err == nil { + runes = runes[:len(runes)-1] + } + if r.pusher(runes) { r.eventBox.Set(EvtReadNew, nil) } } diff --git a/src/reader_test.go b/src/reader_test.go index 00b9e33..bb68e51 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -10,7 +10,7 @@ func TestReadFromCommand(t *testing.T) { strs := []string{} eb := util.NewEventBox() reader := Reader{ - pusher: func(s string) bool { strs = append(strs, s); return true }, + pusher: func(s []rune) bool { strs = append(strs, string(s)); return true }, eventBox: eb} // Check EventBox diff --git a/src/terminal.go b/src/terminal.go index 6c3b147..74c29a0 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -441,10 +441,10 @@ func (t *Terminal) printHeader() { if line >= max { continue } - trimmed, colors, newState := extractColor(&lineStr, state) + trimmed, colors, newState := extractColor(lineStr, state) state = newState item := &Item{ - text: trimmed, + text: []rune(trimmed), index: 0, colors: colors, rank: Rank{0, 0, 0}} @@ -537,7 +537,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c } // Overflow - text := []rune(*item.text) + text := item.text offsets := item.colorOffsets(col2, bold, current) maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3] fullWidth := displayWidth(text) diff --git a/src/tokenizer.go b/src/tokenizer.go index c61b238..a616c6b 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -18,7 +18,7 @@ type Range struct { // Token contains the tokenized part of the strings and its prefix length type Token struct { - text *[]rune + text []rune prefixLength int } @@ -75,8 +75,7 @@ func withPrefixLengths(tokens []string, begin int) []Token { for idx, token := range tokens { // Need to define a new local variable instead of the reused token to take // the pointer to it - runes := []rune(token) - ret[idx] = Token{text: &runes, prefixLength: prefixLength} + ret[idx] = Token{text: []rune(token), prefixLength: prefixLength} prefixLength += len([]rune(token)) } return ret @@ -88,13 +87,13 @@ const ( awkWhite ) -func awkTokenizer(input *string) ([]string, int) { +func awkTokenizer(input []rune) ([]string, int) { // 9, 32 ret := []string{} str := []rune{} prefixLength := 0 state := awkNil - for _, r := range []rune(*input) { + for _, r := range input { white := r == 9 || r == 32 switch state { case awkNil: @@ -126,34 +125,34 @@ func awkTokenizer(input *string) ([]string, int) { } // Tokenize tokenizes the given string with the delimiter -func Tokenize(str *string, delimiter *regexp.Regexp) []Token { +func Tokenize(runes []rune, delimiter *regexp.Regexp) []Token { if delimiter == nil { // AWK-style (\S+\s*) - tokens, prefixLength := awkTokenizer(str) + tokens, prefixLength := awkTokenizer(runes) return withPrefixLengths(tokens, prefixLength) } - tokens := delimiter.FindAllString(*str, -1) + tokens := delimiter.FindAllString(string(runes), -1) return withPrefixLengths(tokens, 0) } -func joinTokens(tokens *[]Token) *string { - ret := "" - for _, token := range *tokens { - ret += string(*token.text) +func joinTokens(tokens []Token) []rune { + ret := []rune{} + for _, token := range tokens { + ret = append(ret, token.text...) } - return &ret + return ret } -func joinTokensAsRunes(tokens *[]Token) *[]rune { +func joinTokensAsRunes(tokens []Token) []rune { ret := []rune{} - for _, token := range *tokens { - ret = append(ret, *token.text...) + for _, token := range tokens { + ret = append(ret, token.text...) } - return &ret + return ret } // Transform is used to transform the input when --with-nth option is given -func Transform(tokens []Token, withNth []Range) *[]Token { +func Transform(tokens []Token, withNth []Range) []Token { transTokens := make([]Token, len(withNth)) numTokens := len(tokens) for idx, r := range withNth { @@ -162,14 +161,14 @@ func Transform(tokens []Token, withNth []Range) *[]Token { if r.begin == r.end { idx := r.begin if idx == rangeEllipsis { - part = append(part, *joinTokensAsRunes(&tokens)...) + part = append(part, joinTokensAsRunes(tokens)...) } else { if idx < 0 { idx += numTokens + 1 } if idx >= 1 && idx <= numTokens { minIdx = idx - 1 - part = append(part, *tokens[idx-1].text...) + part = append(part, tokens[idx-1].text...) } } } else { @@ -196,7 +195,7 @@ func Transform(tokens []Token, withNth []Range) *[]Token { minIdx = util.Max(0, begin-1) for idx := begin; idx <= end; idx++ { if idx >= 1 && idx <= numTokens { - part = append(part, *tokens[idx-1].text...) + part = append(part, tokens[idx-1].text...) } } } @@ -206,7 +205,7 @@ func Transform(tokens []Token, withNth []Range) *[]Token { } else { prefixLength = 0 } - transTokens[idx] = Token{&part, prefixLength} + transTokens[idx] = Token{part, prefixLength} } - return &transTokens + return transTokens } diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index 0362b5a..06603ae 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -43,14 +43,14 @@ func TestParseRange(t *testing.T) { func TestTokenize(t *testing.T) { // AWK-style input := " abc: def: ghi " - tokens := Tokenize(&input, nil) - if string(*tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 { + tokens := Tokenize([]rune(input), nil) + if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 { t.Errorf("%s", tokens) } // With delimiter - tokens = Tokenize(&input, delimiterRegexp(":")) - if string(*tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 { + tokens = Tokenize([]rune(input), delimiterRegexp(":")) + if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 { t.Errorf("%s", tokens) } } @@ -58,39 +58,39 @@ func TestTokenize(t *testing.T) { func TestTransform(t *testing.T) { input := " abc: def: ghi: jkl" { - tokens := Tokenize(&input, nil) + tokens := Tokenize([]rune(input), nil) { ranges := splitNth("1,2,3") tx := Transform(tokens, ranges) - if *joinTokens(tx) != "abc: def: ghi: " { - t.Errorf("%s", *tx) + if string(joinTokens(tx)) != "abc: def: ghi: " { + t.Errorf("%s", tx) } } { ranges := splitNth("1..2,3,2..,1") tx := Transform(tokens, ranges) - if *joinTokens(tx) != "abc: def: ghi: def: ghi: jklabc: " || - len(*tx) != 4 || - string(*(*tx)[0].text) != "abc: def: " || (*tx)[0].prefixLength != 2 || - string(*(*tx)[1].text) != "ghi: " || (*tx)[1].prefixLength != 14 || - string(*(*tx)[2].text) != "def: ghi: jkl" || (*tx)[2].prefixLength != 8 || - string(*(*tx)[3].text) != "abc: " || (*tx)[3].prefixLength != 2 { - t.Errorf("%s", *tx) + if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || + len(tx) != 4 || + string(tx[0].text) != "abc: def: " || tx[0].prefixLength != 2 || + string(tx[1].text) != "ghi: " || tx[1].prefixLength != 14 || + string(tx[2].text) != "def: ghi: jkl" || tx[2].prefixLength != 8 || + string(tx[3].text) != "abc: " || tx[3].prefixLength != 2 { + t.Errorf("%s", tx) } } } { - tokens := Tokenize(&input, delimiterRegexp(":")) + tokens := Tokenize([]rune(input), delimiterRegexp(":")) { ranges := splitNth("1..2,3,2..,1") tx := Transform(tokens, ranges) - if *joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || - len(*tx) != 4 || - string(*(*tx)[0].text) != " abc: def:" || (*tx)[0].prefixLength != 0 || - string(*(*tx)[1].text) != " ghi:" || (*tx)[1].prefixLength != 12 || - string(*(*tx)[2].text) != " def: ghi: jkl" || (*tx)[2].prefixLength != 6 || - string(*(*tx)[3].text) != " abc:" || (*tx)[3].prefixLength != 0 { - t.Errorf("%s", *tx) + if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" || + len(tx) != 4 || + string(tx[0].text) != " abc: def:" || tx[0].prefixLength != 0 || + string(tx[1].text) != " ghi:" || tx[1].prefixLength != 12 || + string(tx[2].text) != " def: ghi: jkl" || tx[2].prefixLength != 6 || + string(tx[3].text) != " abc:" || tx[3].prefixLength != 0 { + t.Errorf("%s", tx) } } } diff --git a/src/util/util.go b/src/util/util.go index 511de1e..a0e1269 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -78,13 +78,13 @@ func IsTty() bool { return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 } -func TrimRight(runes *[]rune) []rune { +func TrimRight(runes []rune) []rune { var i int - for i = len(*runes) - 1; i >= 0; i-- { - char := (*runes)[i] + for i = len(runes) - 1; i >= 0; i-- { + char := runes[i] if char != ' ' && char != '\t' { break } } - return (*runes)[0 : i+1] + return runes[0 : i+1] } From e13bafc1abaea9a9f3142eb58be1e977ca97e114 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 2 Aug 2015 14:25:57 +0900 Subject: [PATCH 365/408] Performance fix - unnecessary rune convertion on --ansi > time cat /tmp/list | fzf-0.10.1-darwin_amd64 --ansi -fqwerty > /dev/null real 0m4.364s user 0m8.231s sys 0m0.820s > time cat /tmp/list | fzf --ansi -fqwerty > /dev/null real 0m4.624s user 0m5.755s sys 0m0.732s --- src/chunklist.go | 6 +++--- src/chunklist_test.go | 14 +++++++------- src/core.go | 38 ++++++++++++++++++++++---------------- src/reader.go | 18 +++--------------- src/reader_test.go | 2 +- src/util/util.go | 16 ++++++++++++++++ 6 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/chunklist.go b/src/chunklist.go index c20ffd4..a953fae 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -7,7 +7,7 @@ type Chunk []*Item // >>> []Item // ItemBuilder is a closure type that builds Item object from a pointer to a // string and an integer -type ItemBuilder func([]rune, int) *Item +type ItemBuilder func([]byte, int) *Item // ChunkList is a list of Chunks type ChunkList struct { @@ -26,7 +26,7 @@ func NewChunkList(trans ItemBuilder) *ChunkList { trans: trans} } -func (c *Chunk) push(trans ItemBuilder, data []rune, index int) bool { +func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool { item := trans(data, index) if item != nil { *c = append(*c, item) @@ -53,7 +53,7 @@ func CountItems(cs []*Chunk) int { } // Push adds the item to the list -func (cl *ChunkList) Push(data []rune) bool { +func (cl *ChunkList) Push(data []byte) bool { cl.mutex.Lock() defer cl.mutex.Unlock() diff --git a/src/chunklist_test.go b/src/chunklist_test.go index faaf04f..26795ef 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -6,8 +6,8 @@ import ( ) func TestChunkList(t *testing.T) { - cl := NewChunkList(func(s []rune, i int) *Item { - return &Item{text: s, rank: Rank{0, 0, uint32(i * 2)}} + cl := NewChunkList(func(s []byte, i int) *Item { + return &Item{text: []rune(string(s)), rank: Rank{0, 0, uint32(i * 2)}} }) // Snapshot @@ -17,8 +17,8 @@ func TestChunkList(t *testing.T) { } // Add some data - cl.Push([]rune("hello")) - cl.Push([]rune("world")) + cl.Push([]byte("hello")) + cl.Push([]byte("world")) // Previously created snapshot should remain the same if len(snapshot) > 0 { @@ -46,7 +46,7 @@ func TestChunkList(t *testing.T) { // Add more data for i := 0; i < chunkSize*2; i++ { - cl.Push([]rune(fmt.Sprintf("item %d", i))) + cl.Push([]byte(fmt.Sprintf("item %d", i))) } // Previous snapshot should remain the same @@ -64,8 +64,8 @@ func TestChunkList(t *testing.T) { t.Error("Unexpected number of items") } - cl.Push([]rune("hello")) - cl.Push([]rune("world")) + cl.Push([]byte("hello")) + cl.Push([]byte("world")) lastChunkCount := len(*snapshot[len(snapshot)-1]) if lastChunkCount != 2 { diff --git a/src/core.go b/src/core.go index c0596e3..fdd1e06 100644 --- a/src/core.go +++ b/src/core.go @@ -63,48 +63,54 @@ func Run(opts *Options) { eventBox := util.NewEventBox() // ANSI code processor - ansiProcessor := func(runes []rune) ([]rune, []ansiOffset) { - // By default, we do nothing - return runes, nil + ansiProcessor := func(data []byte) ([]rune, []ansiOffset) { + return util.BytesToRunes(data), nil + } + ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) { + return data, nil } if opts.Ansi { if opts.Theme != nil { var state *ansiState - ansiProcessor = func(runes []rune) ([]rune, []ansiOffset) { - trimmed, offsets, newState := extractColor(string(runes), state) + ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { + trimmed, offsets, newState := extractColor(string(data), state) state = newState return []rune(trimmed), offsets } } else { // When color is disabled but ansi option is given, // we simply strip out ANSI codes from the input - ansiProcessor = func(runes []rune) ([]rune, []ansiOffset) { - trimmed, _, _ := extractColor(string(runes), nil) + ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { + trimmed, _, _ := extractColor(string(data), nil) return []rune(trimmed), nil } } + ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) { + return ansiProcessor([]byte(string(data))) + } } // Chunk list var chunkList *ChunkList header := make([]string, 0, opts.HeaderLines) if len(opts.WithNth) == 0 { - chunkList = NewChunkList(func(data []rune, index int) *Item { + chunkList = NewChunkList(func(data []byte, index int) *Item { if len(header) < opts.HeaderLines { header = append(header, string(data)) eventBox.Set(EvtHeader, header) return nil } - data, colors := ansiProcessor(data) + runes, colors := ansiProcessor(data) return &Item{ - text: data, + text: runes, index: uint32(index), colors: colors, rank: Rank{0, 0, uint32(index)}} }) } else { - chunkList = NewChunkList(func(data []rune, index int) *Item { - tokens := Tokenize(data, opts.Delimiter) + chunkList = NewChunkList(func(data []byte, index int) *Item { + runes := util.BytesToRunes(data) + tokens := Tokenize(runes, opts.Delimiter) trans := Transform(tokens, opts.WithNth) if len(header) < opts.HeaderLines { header = append(header, string(joinTokens(trans))) @@ -113,12 +119,12 @@ func Run(opts *Options) { } item := Item{ text: joinTokens(trans), - origText: &data, + origText: &runes, index: uint32(index), colors: nil, rank: Rank{0, 0, uint32(index)}} - trimmed, colors := ansiProcessor(item.text) + trimmed, colors := ansiProcessorRunes(item.text) item.text = trimmed item.colors = colors return &item @@ -128,7 +134,7 @@ func Run(opts *Options) { // Reader streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync if !streamingFilter { - reader := Reader{func(data []rune) bool { + reader := Reader{func(data []byte) bool { return chunkList.Push(data) }, eventBox, opts.ReadZero} go reader.ReadSource() @@ -151,7 +157,7 @@ func Run(opts *Options) { if streamingFilter { reader := Reader{ - func(runes []rune) bool { + func(runes []byte) bool { item := chunkList.trans(runes, 0) if item != nil && pattern.MatchItem(item) { fmt.Println(string(item.text)) diff --git a/src/reader.go b/src/reader.go index d979eb6..3e2cf0a 100644 --- a/src/reader.go +++ b/src/reader.go @@ -5,14 +5,13 @@ import ( "io" "os" "os/exec" - "unicode/utf8" "github.com/junegunn/fzf/src/util" ) // Reader reads from command or standard input type Reader struct { - pusher func([]rune) bool + pusher func([]byte) bool eventBox *util.EventBox delimNil bool } @@ -42,21 +41,10 @@ func (r *Reader) feed(src io.Reader) { // end in delim. bytea, err := reader.ReadBytes(delim) if len(bytea) > 0 { - runes := make([]rune, 0, len(bytea)) - for i := 0; i < len(bytea); { - if bytea[i] < utf8.RuneSelf { - runes = append(runes, rune(bytea[i])) - i++ - } else { - r, sz := utf8.DecodeRune(bytea[i:]) - i += sz - runes = append(runes, r) - } - } if err == nil { - runes = runes[:len(runes)-1] + bytea = bytea[:len(bytea)-1] } - if r.pusher(runes) { + if r.pusher(bytea) { r.eventBox.Set(EvtReadNew, nil) } } diff --git a/src/reader_test.go b/src/reader_test.go index bb68e51..d5c218c 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -10,7 +10,7 @@ func TestReadFromCommand(t *testing.T) { strs := []string{} eb := util.NewEventBox() reader := Reader{ - pusher: func(s []rune) bool { strs = append(strs, string(s)); return true }, + pusher: func(s []byte) bool { strs = append(strs, string(s)); return true }, eventBox: eb} // Check EventBox diff --git a/src/util/util.go b/src/util/util.go index a0e1269..eeeb75f 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -6,6 +6,7 @@ import "C" import ( "os" "time" + "unicode/utf8" ) // Max returns the largest integer @@ -88,3 +89,18 @@ func TrimRight(runes []rune) []rune { } return runes[0 : i+1] } + +func BytesToRunes(bytea []byte) []rune { + runes := make([]rune, 0, len(bytea)) + for i := 0; i < len(bytea); { + if bytea[i] < utf8.RuneSelf { + runes = append(runes, rune(bytea[i])) + i++ + } else { + r, sz := utf8.DecodeRune(bytea[i:]) + i += sz + runes = append(runes, r) + } + } + return runes +} From e83ae34a3b488f323efaf60b26c5f08a849d2238 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 2 Aug 2015 14:32:21 +0900 Subject: [PATCH 366/408] Update CHANGELOG - 0.10.2 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bd791f..0bce59b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ CHANGELOG ========= +0.10.2 +------ + +### Fixes and improvements + +- Improvement in perceived response time of queries + - Eager, efficient rune array conversion +- Graceful exit when failed to initialize ncurses (invalid $TERM) +- Improved ranking algorithm when `--nth` option is set +- Changed the default command not to fail when there are files whose names + start with dash + 0.10.1 ------ From 9278f3acd2cb838f8bd9b9b425d30782a7c718c1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 2 Aug 2015 15:02:12 +0900 Subject: [PATCH 367/408] [install] Add --pre option for downloading prerelease binary --- install | 1 + 1 file changed, 1 insertion(+) diff --git a/install b/install index 7b25155..1930fca 100755 --- a/install +++ b/install @@ -1,6 +1,7 @@ #!/usr/bin/env bash version=0.10.1 +[[ "$@" =~ --pre ]] && version=0.10.2 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) From 7833fa739667f2f1ad400f02d6ff3767c4cf6fc8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 2 Aug 2015 15:09:57 +0900 Subject: [PATCH 368/408] [install] Always download binary when --pre is set --- install | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/install b/install index 1930fca..41078f8 100755 --- a/install +++ b/install @@ -1,7 +1,7 @@ #!/usr/bin/env bash -version=0.10.1 -[[ "$@" =~ --pre ]] && version=0.10.2 +[[ "$@" =~ --pre ]] && version=0.10.2 pre=1 || + version=0.10.1 pre=0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) @@ -46,11 +46,13 @@ symlink() { download() { echo "Downloading bin/fzf ..." - if [[ ! $1 =~ dev && -x "$fzf_base"/bin/fzf ]]; then - echo " - Already exists" - check_binary && return - elif [ -x "$fzf_base"/bin/$1 ]; then - symlink $1 && check_binary && return + if [ $pre = 0 ]; then + if [ -x "$fzf_base"/bin/fzf ]; then + echo " - Already exists" + check_binary && return + elif [ -x "$fzf_base"/bin/$1 ]; then + symlink $1 && check_binary && return + fi fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin if [ $? -ne 0 ]; then From ae87f6548af27a293e4b00adc8b5c80fc88a4e92 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 2 Aug 2015 23:54:53 +0900 Subject: [PATCH 369/408] GoLint --- src/algo/algo.go | 1 + src/item.go | 34 +++++++++++++++++----------------- src/util/util.go | 6 +----- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index afc12aa..03266dd 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -161,6 +161,7 @@ func SuffixMatch(caseSensitive bool, input []rune, pattern []rune) (int, int) { return trimmedLen - len(pattern), trimmedLen } +// EqualMatch performs equal-match func EqualMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { if len(runes) != len(pattern) { return -1, -1 diff --git a/src/item.go b/src/item.go index 96f3d23..7a0a244 100644 --- a/src/item.go +++ b/src/item.go @@ -37,14 +37,14 @@ type Rank struct { var rankTiebreak tiebreak // Rank calculates rank of the Item -func (i *Item) Rank(cache bool) Rank { - if cache && (i.rank.matchlen > 0 || i.rank.tiebreak > 0) { - return i.rank +func (item *Item) Rank(cache bool) Rank { + if cache && (item.rank.matchlen > 0 || item.rank.tiebreak > 0) { + return item.rank } matchlen := 0 prevEnd := 0 minBegin := math.MaxUint16 - for _, offset := range i.offsets { + for _, offset := range item.offsets { begin := int(offset[0]) end := int(offset[1]) if prevEnd > begin { @@ -64,21 +64,21 @@ func (i *Item) Rank(cache bool) Rank { switch rankTiebreak { case byLength: // It is guaranteed that .transformed in not null in normal execution - if i.transformed != nil { + if item.transformed != nil { lenSum := 0 - for _, token := range i.transformed { + for _, token := range item.transformed { lenSum += len(token.text) } tiebreak = uint16(lenSum) } else { - tiebreak = uint16(len(i.text)) + tiebreak = uint16(len(item.text)) } case byBegin: - // We can't just look at i.offsets[0][0] because it can be an inverse term + // We can't just look at item.offsets[0][0] because it can be an inverse term tiebreak = uint16(minBegin) case byEnd: if prevEnd > 0 { - tiebreak = uint16(1 + len(i.text) - prevEnd) + tiebreak = uint16(1 + len(item.text) - prevEnd) } else { // Empty offsets due to inverse terms. tiebreak = 1 @@ -86,23 +86,23 @@ func (i *Item) Rank(cache bool) Rank { case byIndex: tiebreak = 1 } - rank := Rank{uint16(matchlen), tiebreak, i.index} + rank := Rank{uint16(matchlen), tiebreak, item.index} if cache { - i.rank = rank + item.rank = rank } return rank } // AsString returns the original string -func (i *Item) AsString() string { - return *i.StringPtr() +func (item *Item) AsString() string { + return *item.StringPtr() } // StringPtr returns the pointer to the original string -func (i *Item) StringPtr() *string { - runes := i.text - if i.origText != nil { - runes = *i.origText +func (item *Item) StringPtr() *string { + runes := item.text + if item.origText != nil { + runes = *item.origText } str := string(runes) return &str diff --git a/src/util/util.go b/src/util/util.go index eeeb75f..aa5f227 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -20,7 +20,7 @@ func Max(first int, items ...int) int { return max } -// Max32 returns the smallest 32-bit integer +// Min32 returns the smallest 32-bit integer func Min32(first int32, second int32) int32 { if first <= second { return first @@ -70,10 +70,6 @@ func DurWithin( return val } -func Between(val int, min int, max int) bool { - return val >= min && val <= max -} - // IsTty returns true is stdin is a terminal func IsTty() bool { return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 From 15c49a3e084d758d3e475da27111732556deaecb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 3 Aug 2015 00:14:34 +0900 Subject: [PATCH 370/408] Fix race condition --- src/terminal.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 74c29a0..aca319a 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -537,7 +537,8 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c } // Overflow - text := item.text + text := make([]rune, len(item.text)) + copy(text, item.text) offsets := item.colorOffsets(col2, bold, current) maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3] fullWidth := displayWidth(text) From c4cf90a3d22a4641633f366fe64b992a62dab13f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 3 Aug 2015 00:21:21 +0900 Subject: [PATCH 371/408] 0.10.2 --- install | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install b/install index 41078f8..ea5a461 100755 --- a/install +++ b/install @@ -1,7 +1,7 @@ #!/usr/bin/env bash [[ "$@" =~ --pre ]] && version=0.10.2 pre=1 || - version=0.10.1 pre=0 + version=0.10.2 pre=0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 3a334cd..cb12acb 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "July 2015" "fzf 0.10.1" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Aug 2015" "fzf 0.10.2" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 91c863a..e19f5f4 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.10.1" + version = "0.10.2" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From bae10a6582f42bab7c8b46e2817ed286ab7807c9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 5 Aug 2015 23:50:38 +0900 Subject: [PATCH 372/408] [install] Add an extra new line character so that it doesn't corrupt file that doesn't end with a new line character. Close #311. --- install | 1 + 1 file changed, 1 insertion(+) diff --git a/install b/install index ea5a461..d27c017 100755 --- a/install +++ b/install @@ -250,6 +250,7 @@ append_line() { if [ -n "$line" ]; then echo " - Already exists: line #$line" else + echo >> "$2" echo "$1" >> "$2" echo " + Added" fi From a7b75c99a5c2dd76163e229a4b6f9f49401272e0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 8 Aug 2015 03:53:46 +0900 Subject: [PATCH 373/408] [install] Stop installer when failed to download the binary Close #312 --- install | 1 + 1 file changed, 1 insertion(+) diff --git a/install b/install index d27c017..0b4b9d9 100755 --- a/install +++ b/install @@ -96,6 +96,7 @@ if [ -n "$binary_error" ]; then echo "No prebuilt binary for $archi ... " else echo " - $binary_error !!!" + exit 1 fi echo "Installing legacy Ruby version ..." From 766427de0c04c64085c5ed907e3fdcc6124fa2dd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 10 Aug 2015 18:34:20 +0900 Subject: [PATCH 374/408] Fix --with-nth performance; avoid regex if possible Close #317 --- CHANGELOG.md | 9 ++++++++ src/options.go | 24 +++++++++++++++------ src/options_test.go | 50 ++++++++++++++++++++++++++++++++++++++++--- src/pattern.go | 4 ++-- src/pattern_test.go | 20 ++++++++--------- src/tokenizer.go | 41 +++++++++++++++++++++++++---------- src/tokenizer_test.go | 4 ++-- 7 files changed, 117 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bce59b..f7ec383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +0.10.3 +------ + +- Fixed slow performance of `--with-nth` when used with `--delimiter` + - Regular expression engine of Golang as of now is very slow, so the fixed + version will treat the given delimiter pattern as a plain string instead + of a regular expression unless it contains special characters and is + a valid regular expression. + 0.10.2 ------ diff --git a/src/options.go b/src/options.go index 6a0d197..35fa8ec 100644 --- a/src/options.go +++ b/src/options.go @@ -104,7 +104,7 @@ type Options struct { Case Case Nth []Range WithNth []Range - Delimiter *regexp.Regexp + Delimiter Delimiter Sort int Tac bool Tiebreak tiebreak @@ -149,7 +149,7 @@ func defaultOptions() *Options { Case: CaseSmart, Nth: make([]Range, 0), WithNth: make([]Range, 0), - Delimiter: nil, + Delimiter: Delimiter{}, Sort: 1000, Tac: false, Tiebreak: byLength, @@ -268,17 +268,27 @@ func splitNth(str string) []Range { return ranges } -func delimiterRegexp(str string) *regexp.Regexp { - rx, e := regexp.Compile(str) - if e != nil { - str = regexp.QuoteMeta(str) +func delimiterRegexp(str string) Delimiter { + // Special handling of \t + str = strings.Replace(str, "\\t", "\t", -1) + + // 1. Pattern does not contain any special character + if regexp.QuoteMeta(str) == str { + return Delimiter{str: &str} } + rx, e := regexp.Compile(str) + // 2. Pattern is not a valid regular expression + if e != nil { + return Delimiter{str: &str} + } + + // 3. Pattern as regular expression. Slow. rx, e = regexp.Compile(fmt.Sprintf("(?:.*?%s)|(?:.+?$)", str)) if e != nil { errorExit("invalid regular expression: " + e.Error()) } - return rx + return Delimiter{regex: rx} } func isAlphabet(char uint8) bool { diff --git a/src/options_test.go b/src/options_test.go index 28a8d9d..e9884f2 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -2,16 +2,60 @@ package fzf import ( "fmt" + "strings" "testing" "github.com/junegunn/fzf/src/curses" ) func TestDelimiterRegex(t *testing.T) { - rx := delimiterRegexp("*") + // Valid regex + delim := delimiterRegexp(".") + if delim.regex == nil || delim.str != nil { + t.Error(delim) + } + // Broken regex -> string + delim = delimiterRegexp("[0-9") + if delim.regex != nil || *delim.str != "[0-9" { + t.Error(delim) + } + // Valid regex + delim = delimiterRegexp("[0-9]") + if strings.Index(delim.regex.String(), "[0-9]") < 0 || delim.str != nil { + t.Error(delim) + } + // Tab character + delim = delimiterRegexp("\t") + if delim.regex != nil || *delim.str != "\t" { + t.Error(delim) + } + // Tab expression + delim = delimiterRegexp("\\t") + if delim.regex != nil || *delim.str != "\t" { + t.Error(delim) + } + // Tabs -> regex + delim = delimiterRegexp("\t+") + if delim.regex == nil || delim.str != nil { + t.Error(delim) + } +} + +func TestDelimiterRegexString(t *testing.T) { + delim := delimiterRegexp("*") + tokens := strings.Split("-*--*---**---", *delim.str) + if delim.regex != nil || tokens[0] != "-" || tokens[1] != "--" || + tokens[2] != "---" || tokens[3] != "" || tokens[4] != "---" { + t.Errorf("%s %s %d", delim, tokens, len(tokens)) + } +} + +func TestDelimiterRegexRegex(t *testing.T) { + delim := delimiterRegexp("--\\*") + rx := delim.regex tokens := rx.FindAllString("-*--*---**---", -1) - if tokens[0] != "-*" || tokens[1] != "--*" || tokens[2] != "---*" || - tokens[3] != "*" || tokens[4] != "---" { + if delim.str != nil || + tokens[0] != "-*--*" || tokens[1] != "---*" || tokens[2] != "*---" { t.Errorf("%s %s %d", rx, tokens, len(tokens)) } } diff --git a/src/pattern.go b/src/pattern.go index 990450a..f83861e 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -42,7 +42,7 @@ type Pattern struct { text []rune terms []term hasInvTerm bool - delimiter *regexp.Regexp + delimiter Delimiter nth []Range procFun map[termType]func(bool, []rune, []rune) (int, int) } @@ -71,7 +71,7 @@ func clearChunkCache() { // BuildPattern builds Pattern object from the given arguments func BuildPattern(mode Mode, caseMode Case, - nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern { + nth []Range, delimiter Delimiter, runes []rune) *Pattern { var asString string switch mode { diff --git a/src/pattern_test.go b/src/pattern_test.go index 8134cdc..c7f5414 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -59,7 +59,7 @@ func TestExact(t *testing.T) { defer clearPatternCache() clearPatternCache() pattern := BuildPattern(ModeExtended, CaseSmart, - []Range{}, nil, []rune("'abc")) + []Range{}, Delimiter{}, []rune("'abc")) sidx, eidx := algo.ExactMatchNaive( pattern.caseSensitive, []rune("aabbcc abc"), pattern.terms[0].text) if sidx != 7 || eidx != 10 { @@ -70,7 +70,7 @@ func TestExact(t *testing.T) { func TestEqual(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("^AbC$")) + pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { sidx, eidx := algo.EqualMatch( @@ -86,17 +86,17 @@ func TestEqual(t *testing.T) { func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() - pat1 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("abc")) + pat1 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat2 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("Abc")) + pat2 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat3 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("abc")) + pat3 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat4 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("Abc")) + pat4 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat5 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("abc")) + pat5 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat6 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("Abc")) + pat6 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, Delimiter{}, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || @@ -109,8 +109,8 @@ func TestCaseSensitivity(t *testing.T) { } func TestOrigTextAndTransformed(t *testing.T) { - pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("jg")) - tokens := Tokenize([]rune("junegunn"), nil) + pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, Delimiter{}, []rune("jg")) + tokens := Tokenize([]rune("junegunn"), Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) origRunes := []rune("junegunn.choi") diff --git a/src/tokenizer.go b/src/tokenizer.go index a616c6b..72deb2b 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -22,6 +22,12 @@ type Token struct { prefixLength int } +// Delimiter for tokenizing the input +type Delimiter struct { + regex *regexp.Regexp + str *string +} + func newRange(begin int, end int) Range { if begin == 1 { begin = rangeEllipsis @@ -68,15 +74,15 @@ func ParseRange(str *string) (Range, bool) { return newRange(n, n), true } -func withPrefixLengths(tokens []string, begin int) []Token { +func withPrefixLengths(tokens [][]rune, begin int) []Token { ret := make([]Token, len(tokens)) prefixLength := begin for idx, token := range tokens { // Need to define a new local variable instead of the reused token to take // the pointer to it - ret[idx] = Token{text: []rune(token), prefixLength: prefixLength} - prefixLength += len([]rune(token)) + ret[idx] = Token{text: token, prefixLength: prefixLength} + prefixLength += len(token) } return ret } @@ -87,9 +93,9 @@ const ( awkWhite ) -func awkTokenizer(input []rune) ([]string, int) { +func awkTokenizer(input []rune) ([][]rune, int) { // 9, 32 - ret := []string{} + ret := [][]rune{} str := []rune{} prefixLength := 0 state := awkNil @@ -112,27 +118,40 @@ func awkTokenizer(input []rune) ([]string, int) { if white { str = append(str, r) } else { - ret = append(ret, string(str)) + ret = append(ret, str) state = awkBlack str = []rune{r} } } } if len(str) > 0 { - ret = append(ret, string(str)) + ret = append(ret, str) } return ret, prefixLength } // Tokenize tokenizes the given string with the delimiter -func Tokenize(runes []rune, delimiter *regexp.Regexp) []Token { - if delimiter == nil { +func Tokenize(runes []rune, delimiter Delimiter) []Token { + if delimiter.str == nil && delimiter.regex == nil { // AWK-style (\S+\s*) tokens, prefixLength := awkTokenizer(runes) return withPrefixLengths(tokens, prefixLength) } - tokens := delimiter.FindAllString(string(runes), -1) - return withPrefixLengths(tokens, 0) + + var tokens []string + if delimiter.str != nil { + tokens = strings.Split(string(runes), *delimiter.str) + for i := 0; i < len(tokens)-1; i++ { + tokens[i] = tokens[i] + *delimiter.str + } + } else if delimiter.regex != nil { + tokens = delimiter.regex.FindAllString(string(runes), -1) + } + asRunes := make([][]rune, len(tokens)) + for i, token := range tokens { + asRunes[i] = []rune(token) + } + return withPrefixLengths(asRunes, 0) } func joinTokens(tokens []Token) []rune { diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index 06603ae..61017b8 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -43,7 +43,7 @@ func TestParseRange(t *testing.T) { func TestTokenize(t *testing.T) { // AWK-style input := " abc: def: ghi " - tokens := Tokenize([]rune(input), nil) + tokens := Tokenize([]rune(input), Delimiter{}) if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 { t.Errorf("%s", tokens) } @@ -58,7 +58,7 @@ func TestTokenize(t *testing.T) { func TestTransform(t *testing.T) { input := " abc: def: ghi: jkl" { - tokens := Tokenize([]rune(input), nil) + tokens := Tokenize([]rune(input), Delimiter{}) { ranges := splitNth("1,2,3") tx := Transform(tokens, ranges) From d0f2c00f9f0a2ded5a94703c30ea127b8f8cb847 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 10 Aug 2015 23:47:03 +0900 Subject: [PATCH 375/408] Fix --with-nth performance; use simpler regular expression Related #317 --- CHANGELOG.md | 1 + src/options.go | 5 ----- src/options_test.go | 22 +++++++++++++--------- src/tokenizer.go | 11 ++++++++++- src/tokenizer_test.go | 9 +++++++++ 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7ec383..41a50a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG version will treat the given delimiter pattern as a plain string instead of a regular expression unless it contains special characters and is a valid regular expression. + - Simpler regular expression for delimiter for better performance 0.10.2 ------ diff --git a/src/options.go b/src/options.go index 35fa8ec..7090066 100644 --- a/src/options.go +++ b/src/options.go @@ -1,7 +1,6 @@ package fzf import ( - "fmt" "io/ioutil" "os" "regexp" @@ -284,10 +283,6 @@ func delimiterRegexp(str string) Delimiter { } // 3. Pattern as regular expression. Slow. - rx, e = regexp.Compile(fmt.Sprintf("(?:.*?%s)|(?:.+?$)", str)) - if e != nil { - errorExit("invalid regular expression: " + e.Error()) - } return Delimiter{regex: rx} } diff --git a/src/options_test.go b/src/options_test.go index e9884f2..1f96f78 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -2,7 +2,6 @@ package fzf import ( "fmt" - "strings" "testing" "github.com/junegunn/fzf/src/curses" @@ -21,7 +20,7 @@ func TestDelimiterRegex(t *testing.T) { } // Valid regex delim = delimiterRegexp("[0-9]") - if strings.Index(delim.regex.String(), "[0-9]") < 0 || delim.str != nil { + if delim.regex.String() != "[0-9]" || delim.str != nil { t.Error(delim) } // Tab character @@ -43,20 +42,25 @@ func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegexString(t *testing.T) { delim := delimiterRegexp("*") - tokens := strings.Split("-*--*---**---", *delim.str) - if delim.regex != nil || tokens[0] != "-" || tokens[1] != "--" || - tokens[2] != "---" || tokens[3] != "" || tokens[4] != "---" { + tokens := Tokenize([]rune("-*--*---**---"), delim) + if delim.regex != nil || + string(tokens[0].text) != "-*" || + string(tokens[1].text) != "--*" || + string(tokens[2].text) != "---*" || + string(tokens[3].text) != "*" || + string(tokens[4].text) != "---" { t.Errorf("%s %s %d", delim, tokens, len(tokens)) } } func TestDelimiterRegexRegex(t *testing.T) { delim := delimiterRegexp("--\\*") - rx := delim.regex - tokens := rx.FindAllString("-*--*---**---", -1) + tokens := Tokenize([]rune("-*--*---**---"), delim) if delim.str != nil || - tokens[0] != "-*--*" || tokens[1] != "---*" || tokens[2] != "*---" { - t.Errorf("%s %s %d", rx, tokens, len(tokens)) + string(tokens[0].text) != "-*--*" || + string(tokens[1].text) != "---*" || + string(tokens[2].text) != "*---" { + t.Errorf("%s %d", tokens, len(tokens)) } } diff --git a/src/tokenizer.go b/src/tokenizer.go index 72deb2b..a8d0400 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -145,7 +145,16 @@ func Tokenize(runes []rune, delimiter Delimiter) []Token { tokens[i] = tokens[i] + *delimiter.str } } else if delimiter.regex != nil { - tokens = delimiter.regex.FindAllString(string(runes), -1) + str := string(runes) + for len(str) > 0 { + loc := delimiter.regex.FindStringIndex(str) + if loc == nil { + loc = []int{0, len(str)} + } + last := util.Max(loc[1], 1) + tokens = append(tokens, str[:last]) + str = str[last:] + } } asRunes := make([][]rune, len(tokens)) for i, token := range tokens { diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index 61017b8..0f95aa1 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -53,6 +53,15 @@ func TestTokenize(t *testing.T) { if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 { t.Errorf("%s", tokens) } + + // With delimiter regex + tokens = Tokenize([]rune(input), delimiterRegexp("\\s+")) + if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || + string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 || + string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 || + string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 { + t.Errorf("%s", tokens) + } } func TestTransform(t *testing.T) { From cacc212f12387f746191ee754f6b6bd58d3441e0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 11 Aug 2015 00:21:09 +0900 Subject: [PATCH 376/408] [install] Prerelease of 0.10.3 --- install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install b/install index 0b4b9d9..63abfb3 100755 --- a/install +++ b/install @@ -1,6 +1,6 @@ #!/usr/bin/env bash -[[ "$@" =~ --pre ]] && version=0.10.2 pre=1 || +[[ "$@" =~ --pre ]] && version=0.10.3 pre=1 || version=0.10.2 pre=0 cd $(dirname $BASH_SOURCE) From 8156e9894e9f05cfea9043af44d0258ad2a6721d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 12 Aug 2015 02:09:46 +0900 Subject: [PATCH 377/408] 0.10.3 --- install | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install b/install index 63abfb3..dae6a74 100755 --- a/install +++ b/install @@ -1,7 +1,7 @@ #!/usr/bin/env bash [[ "$@" =~ --pre ]] && version=0.10.3 pre=1 || - version=0.10.2 pre=0 + version=0.10.3 pre=0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index cb12acb..06557b9 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Aug 2015" "fzf 0.10.2" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Aug 2015" "fzf 0.10.3" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index e19f5f4..27b69d4 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.10.2" + version = "0.10.3" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From e5fec408c4a79c9a537f07b6adcbc94554579c53 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 15 Aug 2015 23:53:11 +0900 Subject: [PATCH 378/408] [vim] tab split instead of tabedit --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index d81784d..c240773 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -358,7 +358,7 @@ endfunction let s:default_action = { \ 'ctrl-m': 'e', - \ 'ctrl-t': 'tabedit', + \ 'ctrl-t': 'tab split', \ 'ctrl-x': 'split', \ 'ctrl-v': 'vsplit' } From c601fc6437f864c440b9d35c87604e48f2a48446 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 15 Aug 2015 23:53:27 +0900 Subject: [PATCH 379/408] [vim] Open silently --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index c240773..0a88cf0 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -372,7 +372,7 @@ function! s:cmd_callback(lines) abort let autochdir = &autochdir set noautochdir for item in a:lines - execute cmd s:escape(item) + execute 'silent' cmd s:escape(item) endfor finally let &autochdir = autochdir From 7ae877bd3a66759fee2b97fd1212f761a16738c6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 16 Aug 2015 00:04:45 +0900 Subject: [PATCH 380/408] [vim] Handle single/double quote characters in 'dir' option --- plugin/fzf.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 0a88cf0..e31833c 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -80,7 +80,7 @@ function! s:shellesc(arg) endfunction function! s:escape(path) - return escape(a:path, ' %#\') + return escape(a:path, ' %#''"\') endfunction " Upgrade legacy options @@ -384,7 +384,7 @@ function! s:cmd(bang, ...) abort let args = extend(['--expect='.join(keys(s:action), ',')], a:000) let opts = {} if len(args) > 0 && isdirectory(expand(args[-1])) - let opts.dir = remove(args, -1) + let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g') endif if !a:bang let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height)) From 60336c74239c7522f5998e65a8365589cc188482 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 16 Aug 2015 02:47:52 +0900 Subject: [PATCH 381/408] Remove Vim examples from README.md --- README.md | 74 +++---------------------------------------------------- 1 file changed, 3 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 64d9379..f9cb997 100644 --- a/README.md +++ b/README.md @@ -286,10 +286,8 @@ customization. #### `fzf#run([options])` -For more advanced uses, you can call `fzf#run()` function which returns the list -of the selected items. - -`fzf#run()` may take an options-dictionary: +For more advanced uses, you can use `fzf#run()` function with the following +options. | Option name | Type | Description | | -------------------------- | ------------- | ---------------------------------------------------------------- | @@ -305,65 +303,7 @@ of the selected items. | `launcher` | string | External terminal emulator to start fzf with (GVim only) | | `launcher` | funcref | Function for generating `launcher` string (GVim only) | -_However on Neovim `fzf#run` is asynchronous and does not return values so you -should use `sink` or `sink*` to process the output from fzf._ - -##### Examples - -If `sink` option is not given, `fzf#run` will simply return the list. - -```vim -let items = fzf#run({ 'options': '-m +c', 'dir': '~', 'source': 'ls' }) -``` - -But if `sink` is given as a string, the command will be executed for each -selected item. - -```vim -" Each selected item will be opened in a new tab -let items = fzf#run({ 'sink': 'tabe', 'options': '-m +c', 'dir': '~', 'source': 'ls' }) -``` - -We can also use a Vim list as the source as follows: - -```vim -" Choose a color scheme with fzf -nnoremap C :call fzf#run({ -\ 'source': -\ map(split(globpath(&rtp, "colors/*.vim"), "\n"), -\ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"), -\ 'sink': 'colo', -\ 'options': '+m', -\ 'left': 20, -\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s' -\ }) -``` - -`sink` option can be a function reference. The following example creates a -handy mapping that selects an open buffer. - -```vim -" List of buffers -function! s:buflist() - redir => ls - silent ls - redir END - return split(ls, '\n') -endfunction - -function! s:bufopen(e) - execute 'buffer' matchstr(a:e, '^[ 0-9]*') -endfunction - -nnoremap :call fzf#run({ -\ 'source': reverse(buflist()), -\ 'sink': function('bufopen'), -\ 'options': '+m', -\ 'down': len(buflist()) + 2 -\ }) -``` - -More examples can be found on [the wiki +Examples can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). Tips @@ -425,14 +365,6 @@ of fzf to a temporary file. fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result) ``` -#### Handling UTF-8 NFD paths on OSX - -Use iconv to convert NFD paths to NFC: - -```sh -find . | iconv -f utf-8-mac -t utf8//ignore | fzf -``` - License ------- From 55d566b72f0f5743fe11349635a8dfdc6a3259ac Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 18 Aug 2015 12:03:08 +0900 Subject: [PATCH 382/408] Revert "[vim] Open silently" This reverts commit c601fc6437f864c440b9d35c87604e48f2a48446. --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index e31833c..a350c29 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -372,7 +372,7 @@ function! s:cmd_callback(lines) abort let autochdir = &autochdir set noautochdir for item in a:lines - execute 'silent' cmd s:escape(item) + execute cmd s:escape(item) endfor finally let &autochdir = autochdir From 1c31e07d34ab53e83106bd40c28a8d5bc5ea2f46 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 19 Aug 2015 19:42:06 +0900 Subject: [PATCH 383/408] [install] Improve error message --- install | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/install b/install index dae6a74..1e59e63 100755 --- a/install +++ b/install @@ -21,16 +21,21 @@ ask() { check_binary() { echo -n " - Checking fzf executable ... " - local output=$("$fzf_base"/bin/fzf --version 2>&1) - if [ "$version" = "$output" ]; then + local output + output=$("$fzf_base"/bin/fzf --version 2>&1) + if [ $? -ne 0 ]; then + echo "Error: $output" + binary_error="Invalid binary" + elif [ "$version" != "$output" ]; then + echo "$output != $version" + binary_error="Invalid version" + else echo "$output" binary_error="" - else - echo "$output != $version" - rm -f "$fzf_base"/bin/fzf - binary_error="Invalid binary" - return 1 + return 0 fi + rm -f "$fzf_base"/bin/fzf + return 1 } symlink() { From 148f21415a9d2019617d3620e661caf57a677ade Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 22 Aug 2015 19:30:21 +0900 Subject: [PATCH 384/408] Mention fzf.vim project --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f9cb997..2bbdff7 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,9 @@ export FZF_COMPLETION_OPTS='+c -x' Usage as Vim plugin ------------------- +This repository only enables basic integration with Vim. If you're looking for +more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project. + (Note: To use fzf in GVim, an external terminal emulator is required.) #### `:FZF[!]` From 8fb31e1b4d25ef20d039e919e8d7712d2ad6a96c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 24 Aug 2015 01:52:16 +0900 Subject: [PATCH 385/408] [vim] Escape % and # when running source command with :! --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index a350c29..58e481f 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -218,7 +218,7 @@ function! s:execute(dict, command, temps) else let command = a:command endif - execute 'silent !'.command + execute 'silent !'.escape(command, '%#') redraw! if v:shell_error " Do not print error message on exit status 1 From 80ed02e72ee3c5d408d1b8f5a81fb43e18a87ed1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 26 Aug 2015 23:35:31 +0900 Subject: [PATCH 386/408] Add failing test case for #329 --- test/test_go.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/test_go.rb b/test/test_go.rb index c21c94e..ba229bf 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -774,6 +774,16 @@ class TestGoFZF < TestBase tmux.until { |lines| lines.any? { |l| l.include? 'Invalid $TERM: xxx' } } end + def test_with_nth + writelines tempname, ['hello world ', 'byebye'] + assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp + end + + def test_with_nth_ansi + writelines tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'] + assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi`.chomp + end + private def writelines path, lines File.unlink path while File.exists? path From 5a72dc6922b51977d46d92a649f5c9ce67003bcf Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 26 Aug 2015 23:58:18 +0900 Subject: [PATCH 387/408] Fix #329 - Trim ANSI codes from output when --ansi & --with-nth are set --- src/item.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/item.go b/src/item.go index 7a0a244..2ab8a78 100644 --- a/src/item.go +++ b/src/item.go @@ -100,11 +100,11 @@ func (item *Item) AsString() string { // StringPtr returns the pointer to the original string func (item *Item) StringPtr() *string { - runes := item.text if item.origText != nil { - runes = *item.origText + trimmed, _, _ := extractColor(string(*item.origText), nil) + return &trimmed } - str := string(runes) + str := string(item.text) return &str } From 7f7741099bd30649adac92551912b97475b9c202 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 27 Aug 2015 03:28:05 +0900 Subject: [PATCH 388/408] make linux-static (#322) --- src/Dockerfile.ubuntu | 2 +- src/Makefile | 65 +++++++++++++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu index 4778a6d..d9de120 100644 --- a/src/Dockerfile.ubuntu +++ b/src/Dockerfile.ubuntu @@ -3,7 +3,7 @@ MAINTAINER Junegunn Choi # apt-get RUN apt-get update && apt-get -y upgrade && \ - apt-get install -y --force-yes git curl build-essential libncurses-dev + apt-get install -y --force-yes git curl build-essential libncurses-dev libgpm-dev # Install Go 1.4 RUN cd / && curl \ diff --git a/src/Makefile b/src/Makefile index 2604c3b..33c23ab 100644 --- a/src/Makefile +++ b/src/Makefile @@ -5,8 +5,19 @@ endif UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) GOOS := darwin + LDFLAGS := +ifdef STATIC +$(error Static linking not possible on OS X) +endif else ifeq ($(UNAME_S),Linux) GOOS := linux +ifdef STATIC + SUFFIX := -static + LDFLAGS := --ldflags '-extldflags "-static -ltinfo -lgpm"' +else + SUFFIX := + LDFLAGS := +endif endif ifneq ($(shell uname -m),x86_64) @@ -16,21 +27,24 @@ endif SOURCES := $(wildcard *.go */*.go) BINDIR := ../bin -BINARY32 := fzf-$(GOOS)_386 -BINARY64 := fzf-$(GOOS)_amd64 +BINARY32 := fzf-$(GOOS)_386$(SUFFIX) +BINARY64 := fzf-$(GOOS)_amd64$(SUFFIX) VERSION = $(shell fzf/$(BINARY64) --version) -RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 -RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 +RELEASE32 = fzf-$(VERSION)-$(GOOS)_386$(SUFFIX) +RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64$(SUFFIX) all: release release: build - cd fzf && \ - cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \ - cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ - rm $(RELEASE32) $(RELEASE64) + -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) + cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ + rm -f $(RELEASE32) $(RELEASE64) +ifndef STATIC build: test fzf/$(BINARY32) fzf/$(BINARY64) +else +build: test fzf/$(BINARY64) +endif test: go get @@ -42,13 +56,13 @@ uninstall: rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64) clean: - cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz + cd fzf && rm -f fzf-* fzf/$(BINARY32): $(SOURCES) cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32) fzf/$(BINARY64): $(SOURCES) - cd fzf && go build -o $(BINARY64) + cd fzf && go build $(LDFLAGS) -o $(BINARY64) $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) cp -f fzf/$(BINARY64) $(BINDIR) @@ -57,18 +71,27 @@ $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) $(BINDIR): mkdir -p $@ -# Linux distribution to build fzf on -DISTRO := arch +docker-arch: + docker build -t junegunn/arch-sandbox - < Dockerfile.arch -docker: - docker build -t junegunn/$(DISTRO)-sandbox - < Dockerfile.$(DISTRO) +docker-ubuntu: + docker build -t junegunn/arch-sandbox - < Dockerfile.ubuntu -linux: docker - docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ - /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make' - -$(DISTRO): docker - docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ +arch: docker-arch + docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' -.PHONY: all build release test install uninstall clean docker linux $(DISTRO) +ubuntu: docker-ubuntu + docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ + sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + +linux: docker-arch + docker run -i -t -v $(GOPATH):/go junegunn/arch-sandbox \ + /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make' + +linux-static: docker-ubuntu + docker run -i -t -v $(GOPATH):/go junegunn/ubuntu-sandbox \ + /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make STATIC=1' + +.PHONY: all build release test install uninstall clean docker \ + linux linux-static arch ubuntu docker-arch docker-ubuntu From 0d66ad23c61e799ff05c2291b0e8c9540ff7e23e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 27 Aug 2015 22:48:42 +0900 Subject: [PATCH 389/408] Fix build script --- src/Dockerfile.arch | 1 + src/Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch index b5fd7c0..d014651 100644 --- a/src/Dockerfile.arch +++ b/src/Dockerfile.arch @@ -2,6 +2,7 @@ FROM base/archlinux:2014.07.03 MAINTAINER Junegunn Choi # apt-get +RUN pacman-key --populate archlinux && pacman-key --refresh-keys RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git # Install Go 1.4 diff --git a/src/Makefile b/src/Makefile index 33c23ab..3304f57 100644 --- a/src/Makefile +++ b/src/Makefile @@ -75,7 +75,7 @@ docker-arch: docker build -t junegunn/arch-sandbox - < Dockerfile.arch docker-ubuntu: - docker build -t junegunn/arch-sandbox - < Dockerfile.ubuntu + docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu arch: docker-arch docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ From 1de4cc3ba862ace3f78db395a2bd1dfa21e4798e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 27 Aug 2015 22:50:34 +0900 Subject: [PATCH 390/408] [install] Fall back statically-linked binary on 64-bit linux Close #322 --- install | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/install b/install index 1e59e63..d11c3b0 100755 --- a/install +++ b/install @@ -80,7 +80,12 @@ download() { return fi - chmod +x $1 && symlink $1 && check_binary + chmod +x $1 && symlink $1 || return 1 + if [[ $1 =~ linux_amd64$ ]]; then + check_binary || download $1-static + else + check_binary + fi } # Try to download binary executable From 698e8008df42d863af01a8da81f560f295f728ea Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 28 Aug 2015 18:38:47 +0900 Subject: [PATCH 391/408] [vim] Dynamic height specification for 'up' and 'down' options Values for 'up' and 'down' can be written with ~ prefix. Only applies when the source is a Vim list. e.g. { 'source': range(10), 'down': '~40%' } --- plugin/fzf.vim | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 58e481f..f0461b2 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -164,7 +164,13 @@ function! s:fzf_tmux(dict) let size = '' for o in ['up', 'down', 'left', 'right'] if s:present(a:dict, o) - let size = '-'.o[0].(a:dict[o] == 1 ? '' : a:dict[o]) + let spec = a:dict[o] + if (o == 'up' || o == 'down') && spec[0] == '~' + let size = '-'.o[0].s:calc_size(&lines, spec[1:], a:dict) + else + " Legacy boolean option + let size = '-'.o[0].(spec == 1 ? '' : spec) + endif break endif endfor @@ -244,12 +250,21 @@ function! s:execute_tmux(dict, command, temps) return s:callback(a:dict, a:temps) endfunction -function! s:calc_size(max, val) +function! s:calc_size(max, val, dict) if a:val =~ '%$' - return a:max * str2nr(a:val[:-2]) / 100 + let size = a:max * str2nr(a:val[:-2]) / 100 else - return min([a:max, a:val]) + let size = min([a:max, str2nr(a:val)]) endif + + let srcsz = -1 + if type(get(a:dict, 'source', 0)) == type([]) + let srcsz = len(a:dict.source) + endif + + let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS + let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2 + return srcsz >= 0 ? min([srcsz + margin, size]) : size endfunction function! s:getpos() @@ -268,7 +283,11 @@ function! s:split(dict) let val = get(a:dict, dir, '') if !empty(val) let [cmd, resz, max] = triple - let sz = s:calc_size(max, val) + if (dir == 'up' || dir == 'down') && val[0] == '~' + let sz = s:calc_size(max, val[1:], a:dict) + else + let sz = s:calc_size(max, val, {}) + endif execute cmd sz.'new' execute resz sz return From 90b0cd44ac8fb9a6965c3dcf050f0e6aa2e90121 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 28 Aug 2015 21:23:10 +0900 Subject: [PATCH 392/408] Should not strip ANSI codes when --ansi is not set --- src/core.go | 4 ++-- src/item.go | 14 +++++++++----- src/terminal.go | 8 +++++--- test/test_go.rb | 6 ++++++ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/core.go b/src/core.go index fdd1e06..4f07215 100644 --- a/src/core.go +++ b/src/core.go @@ -174,7 +174,7 @@ func Run(opts *Options) { chunks: snapshot, pattern: pattern}) for i := 0; i < merger.Length(); i++ { - fmt.Println(merger.Get(i).AsString()) + fmt.Println(merger.Get(i).AsString(opts.Ansi)) } } os.Exit(0) @@ -250,7 +250,7 @@ func Run(opts *Options) { fmt.Println() } for i := 0; i < count; i++ { - fmt.Println(val.Get(i).AsString()) + fmt.Println(val.Get(i).AsString(opts.Ansi)) } os.Exit(0) } diff --git a/src/item.go b/src/item.go index 2ab8a78..12ca3df 100644 --- a/src/item.go +++ b/src/item.go @@ -94,15 +94,19 @@ func (item *Item) Rank(cache bool) Rank { } // AsString returns the original string -func (item *Item) AsString() string { - return *item.StringPtr() +func (item *Item) AsString(stripAnsi bool) string { + return *item.StringPtr(stripAnsi) } // StringPtr returns the pointer to the original string -func (item *Item) StringPtr() *string { +func (item *Item) StringPtr(stripAnsi bool) *string { if item.origText != nil { - trimmed, _, _ := extractColor(string(*item.origText), nil) - return &trimmed + if stripAnsi { + trimmed, _, _ := extractColor(string(*item.origText), nil) + return &trimmed + } + orig := string(*item.origText) + return &orig } str := string(item.text) return &str diff --git a/src/terminal.go b/src/terminal.go index aca319a..053ed78 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -42,6 +42,7 @@ type Terminal struct { history *History cycle bool header []string + ansi bool margin [4]string marginInt [4]int count int @@ -207,6 +208,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { marginInt: [4]int{0, 0, 0, 0}, cycle: opts.Cycle, header: opts.Header, + ansi: opts.Ansi, reading: true, merger: EmptyMerger, selected: make(map[uint32]selectedItem), @@ -288,7 +290,7 @@ func (t *Terminal) output() { if len(t.selected) == 0 { cnt := t.merger.Length() if cnt > 0 && cnt > t.cy { - fmt.Println(t.merger.Get(t.cy).AsString()) + fmt.Println(t.merger.Get(t.cy).AsString(t.ansi)) } } else { sels := make([]selectedItem, 0, len(t.selected)) @@ -805,7 +807,7 @@ func (t *Terminal) Loop() { } selectItem := func(item *Item) bool { if _, found := t.selected[item.index]; !found { - t.selected[item.index] = selectedItem{time.Now(), item.StringPtr()} + t.selected[item.index] = selectedItem{time.Now(), item.StringPtr(t.ansi)} return true } return false @@ -843,7 +845,7 @@ func (t *Terminal) Loop() { case actExecute: if t.cy >= 0 && t.cy < t.merger.Length() { item := t.merger.Get(t.cy) - executeCommand(t.execmap[mapkey], item.AsString()) + executeCommand(t.execmap[mapkey], item.AsString(t.ansi)) } case actInvalid: t.mutex.Unlock() diff --git a/test/test_go.rb b/test/test_go.rb index ba229bf..377af22 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -784,6 +784,12 @@ class TestGoFZF < TestBase assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi`.chomp end + def test_with_nth_no_ansi + src = "\x1b[33mhello \x1b[34;1mworld\x1b[m " + writelines tempname, [src, 'byebye'] + assert_equal src, `cat #{tempname} | #{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi`.chomp + end + private def writelines path, lines File.unlink path while File.exists? path From 9a2d9ad947bfdec98987bf965d38cfd26ae60356 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 29 Aug 2015 02:36:27 +0900 Subject: [PATCH 393/408] 0.10.4 --- CHANGELOG.md | 5 +++++ install | 4 ++-- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41a50a9..633d264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.10.4 +------ + +- Fixed to remove ANSI code from output when `--with-nth` is set + 0.10.3 ------ diff --git a/install b/install index d11c3b0..b04d4d0 100755 --- a/install +++ b/install @@ -1,7 +1,7 @@ #!/usr/bin/env bash -[[ "$@" =~ --pre ]] && version=0.10.3 pre=1 || - version=0.10.3 pre=0 +[[ "$@" =~ --pre ]] && version=0.10.4 pre=1 || + version=0.10.4 pre=0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 06557b9..d4ec1e7 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Aug 2015" "fzf 0.10.3" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Aug 2015" "fzf 0.10.4" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 27b69d4..e335a57 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.10.3" + version = "0.10.4" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 5baf1c55362e08c05a80ef822f83598839559093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kalbarczyk?= Date: Sat, 29 Aug 2015 20:09:10 +0200 Subject: [PATCH 394/408] fix fish streams --- shell/key-bindings.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index c79d0fd..a08df3d 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -19,7 +19,7 @@ function fzf_key_bindings -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-" - eval $FZF_CTRL_T_COMMAND | eval (__fzfcmd) -m > $TMPDIR/fzf.result + eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m > $TMPDIR/fzf.result" and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) commandline -f repaint rm -f $TMPDIR/fzf.result From 5660cebaf66b47f927d468422b7c1ad516c32916 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 1 Sep 2015 00:51:28 +0900 Subject: [PATCH 395/408] [zsh-completion] Temporarily unset shwordsplit (#328) --- shell/completion.zsh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 57c4222..f45c538 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -99,7 +99,11 @@ EOF } fzf-completion() { - local tokens cmd prefix trigger tail fzf matches lbuf d_cmds + local tokens cmd prefix trigger tail fzf matches lbuf d_cmds sws + if setopt | grep shwordsplit > /dev/null; then + sws=1 + unsetopt shwordsplit + fi # http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags @@ -148,6 +152,7 @@ fzf-completion() { else eval "zle ${fzf_default_completion:-expand-or-complete}" fi + [ -n "$sws" ] && setopt shwordsplit } [ -z "$fzf_default_completion" ] && From 62e01a2a62ac2d4a8ee4c87723f689516e710828 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 1 Sep 2015 01:13:35 +0900 Subject: [PATCH 396/408] [vim] Escape newline character when running fzf with :! Fixes Helptags! command from fzf.vim --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index f0461b2..65b4aff 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -224,7 +224,7 @@ function! s:execute(dict, command, temps) else let command = a:command endif - execute 'silent !'.escape(command, '%#') + execute 'silent !'.escape(substitute(command, '\n', '\\n', 'g'), '%#') redraw! if v:shell_error " Do not print error message on exit status 1 From ac160f98a8f5aa9ad148a7c6e694b10586652824 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 5 Sep 2015 21:39:12 +0900 Subject: [PATCH 397/408] [gvim] Fix #342 - Should not escape launcher part of the command --- plugin/fzf.vim | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 65b4aff..eea1ffd 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -217,14 +217,15 @@ let s:launcher = function('s:xterm_launcher') function! s:execute(dict, command, temps) call s:pushd(a:dict) silent! !clear 2> /dev/null + let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#') if has('gui_running') let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher - let command = printf(fmt, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'") + let command = printf(fmt, "'".substitute(escaped, "'", "'\"'\"'", 'g')."'") else - let command = a:command + let command = escaped endif - execute 'silent !'.escape(substitute(command, '\n', '\\n', 'g'), '%#') + execute 'silent !'.command redraw! if v:shell_error " Do not print error message on exit status 1 From 0a22142d88ae6cf65d3c9801978241554c3ee968 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 7 Sep 2015 18:40:39 +0900 Subject: [PATCH 398/408] [fzf-tmux] Fix #343 - Escape backticks in --query --- bin/fzf-tmux | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index abed1ee..5f690e5 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -117,7 +117,9 @@ mkfifo $fifo3 # Build arguments to fzf opts="" for arg in "${args[@]}"; do - opts="$opts \"${arg//\"/\\\"}\"" + arg="${arg//\"/\\\"}" + arg="${arg//\`/\\\`}" + opts="$opts \"$arg\"" done if [ -n "$term" -o -t 0 ]; then From 9017e297417bc20c89e1e7c9ce47f1c2fbbfd5fc Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 12 Sep 2015 11:00:30 +0900 Subject: [PATCH 399/408] Make it possible to unquote the term in extended-exact mode Close #338 --- README.md | 3 ++- src/pattern.go | 3 +++ src/pattern_test.go | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2bbdff7..6ec5fbc 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,8 @@ such as: `^music .mp3$ sbtrkt !rmx` | `!'fire` | Items that do not include `fire` | inverse-exact-match | If you don't need fuzzy matching and do not wish to "quote" every word, start -fzf with `-e` or `--extended-exact` option. +fzf with `-e` or `--extended-exact` option. Note that in `--extended-exact` +mode, `'`-prefix "unquotes" the term. #### Environment variables diff --git a/src/pattern.go b/src/pattern.go index f83861e..cfeb68d 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -151,6 +151,9 @@ func parseTerms(mode Mode, caseMode Case, str string) []term { if mode == ModeExtended { typ = termExact text = text[1:] + } else if mode == ModeExtendedExact { + typ = termFuzzy + text = text[1:] } } else if strings.HasPrefix(text, "^") { if strings.HasSuffix(text, "$") { diff --git a/src/pattern_test.go b/src/pattern_test.go index c7f5414..66f5d41 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -37,11 +37,11 @@ func TestParseTermsExtendedExact(t *testing.T) { "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") if len(terms) != 8 || terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 || - terms[1].typ != termExact || terms[1].inv || len(terms[1].text) != 4 || + terms[1].typ != termFuzzy || terms[1].inv || len(terms[1].text) != 3 || terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 || terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 || terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 || - terms[5].typ != termExact || !terms[5].inv || len(terms[5].text) != 4 || + terms[5].typ != termFuzzy || !terms[5].inv || len(terms[5].text) != 3 || terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 || terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 { t.Errorf("%s", terms) From 64443221aab288a3069d01cdaf86706c6c1d91f3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 12 Sep 2015 11:37:55 +0900 Subject: [PATCH 400/408] Fix #344 - Backward scan when `--tiebreak=end` --- src/algo/algo.go | 60 +++++++++++++++++++++++++++++-------------- src/algo/algo_test.go | 56 ++++++++++++++++++++++++++-------------- src/core.go | 3 ++- src/pattern.go | 18 +++++++------ src/pattern_test.go | 22 ++++++++-------- test/test_go.rb | 11 ++++++++ 6 files changed, 112 insertions(+), 58 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 03266dd..ac7bd8b 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -15,8 +15,15 @@ import ( * In short: They try to do as little work as possible. */ +func runeAt(runes []rune, index int, max int, forward bool) rune { + if forward { + return runes[index] + } + return runes[max-index-1] +} + // FuzzyMatch performs fuzzy-match -func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { +func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { if len(pattern) == 0 { return 0, 0 } @@ -34,7 +41,11 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { sidx := -1 eidx := -1 - for index, char := range runes { + lenRunes := len(runes) + lenPattern := len(pattern) + + for index := range runes { + char := runeAt(runes, index, lenRunes, forward) // This is considerably faster than blindly applying strings.ToLower to the // whole string if !caseSensitive { @@ -47,11 +58,12 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { char = unicode.To(unicode.LowerCase, char) } } - if char == pattern[pidx] { + pchar := runeAt(pattern, pidx, lenPattern, forward) + if char == pchar { if sidx < 0 { sidx = index } - if pidx++; pidx == len(pattern) { + if pidx++; pidx == lenPattern { eidx = index + 1 break } @@ -61,7 +73,7 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { if sidx >= 0 && eidx >= 0 { pidx-- for index := eidx - 1; index >= sidx; index-- { - char := runes[index] + char := runeAt(runes, index, lenRunes, forward) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -69,14 +81,19 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { char = unicode.To(unicode.LowerCase, char) } } - if char == pattern[pidx] { + + pchar := runeAt(pattern, pidx, lenPattern, forward) + if char == pchar { if pidx--; pidx < 0 { sidx = index break } } } - return sidx, eidx + if forward { + return sidx, eidx + } + return lenRunes - eidx, lenRunes - sidx } return -1, -1 } @@ -88,20 +105,21 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { // // We might try to implement better algorithms in the future: // http://en.wikipedia.org/wiki/String_searching_algorithm -func ExactMatchNaive(caseSensitive bool, runes []rune, pattern []rune) (int, int) { +func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { if len(pattern) == 0 { return 0, 0 } - numRunes := len(runes) - plen := len(pattern) - if numRunes < plen { + lenRunes := len(runes) + lenPattern := len(pattern) + + if lenRunes < lenPattern { return -1, -1 } pidx := 0 - for index := 0; index < numRunes; index++ { - char := runes[index] + for index := 0; index < lenRunes; index++ { + char := runeAt(runes, index, lenRunes, forward) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -109,10 +127,14 @@ func ExactMatchNaive(caseSensitive bool, runes []rune, pattern []rune) (int, int char = unicode.To(unicode.LowerCase, char) } } - if pattern[pidx] == char { + pchar := runeAt(pattern, pidx, lenPattern, forward) + if pchar == char { pidx++ - if pidx == plen { - return index - plen + 1, index + 1 + if pidx == lenPattern { + if forward { + return index - lenPattern + 1, index + 1 + } + return lenRunes - (index + 1), lenRunes - (index - lenPattern + 1) } } else { index -= pidx @@ -123,7 +145,7 @@ func ExactMatchNaive(caseSensitive bool, runes []rune, pattern []rune) (int, int } // PrefixMatch performs prefix-match -func PrefixMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { +func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { if len(runes) < len(pattern) { return -1, -1 } @@ -141,7 +163,7 @@ func PrefixMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { } // SuffixMatch performs suffix-match -func SuffixMatch(caseSensitive bool, input []rune, pattern []rune) (int, int) { +func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) (int, int) { runes := util.TrimRight(input) trimmedLen := len(runes) diff := trimmedLen - len(pattern) @@ -162,7 +184,7 @@ func SuffixMatch(caseSensitive bool, input []rune, pattern []rune) (int, int) { } // EqualMatch performs equal-match -func EqualMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { +func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { if len(runes) != len(pattern) { return -1, -1 } diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index db24196..95a020b 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -5,11 +5,11 @@ import ( "testing" ) -func assertMatch(t *testing.T, fun func(bool, []rune, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { +func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) (int, int), caseSensitive bool, forward bool, input string, pattern string, sidx int, eidx int) { if !caseSensitive { pattern = strings.ToLower(pattern) } - s, e := fun(caseSensitive, []rune(input), []rune(pattern)) + s, e := fun(caseSensitive, forward, []rune(input), []rune(pattern)) if s != sidx { t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) } @@ -19,33 +19,51 @@ func assertMatch(t *testing.T, fun func(bool, []rune, []rune) (int, int), caseSe } func TestFuzzyMatch(t *testing.T) { - assertMatch(t, FuzzyMatch, false, "fooBarbaz", "oBZ", 2, 9) - assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBZ", -1, -1) - assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBz", 2, 9) - assertMatch(t, FuzzyMatch, true, "fooBarbaz", "fooBarbazz", -1, -1) + assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9) + assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1) + assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9) + assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1) +} + +func TestFuzzyMatchBackward(t *testing.T) { + assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4) + assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9) } func TestExactMatchNaive(t *testing.T) { - assertMatch(t, ExactMatchNaive, false, "fooBarbaz", "oBA", 2, 5) - assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "oBA", -1, -1) - assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "fooBarbazz", -1, -1) + for _, dir := range []bool{true, false} { + assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5) + assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1) + assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1) + } +} + +func TestExactMatchNaiveBackward(t *testing.T) { + assertMatch(t, FuzzyMatch, false, true, "foobar foob", "oo", 1, 3) + assertMatch(t, FuzzyMatch, false, false, "foobar foob", "oo", 8, 10) } func TestPrefixMatch(t *testing.T) { - assertMatch(t, PrefixMatch, false, "fooBarbaz", "Foo", 0, 3) - assertMatch(t, PrefixMatch, true, "fooBarbaz", "Foo", -1, -1) - assertMatch(t, PrefixMatch, false, "fooBarbaz", "baz", -1, -1) + for _, dir := range []bool{true, false} { + assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3) + assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1) + assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1) + } } func TestSuffixMatch(t *testing.T) { - assertMatch(t, SuffixMatch, false, "fooBarbaz", "Foo", -1, -1) - assertMatch(t, SuffixMatch, false, "fooBarbaz", "baz", 6, 9) - assertMatch(t, SuffixMatch, true, "fooBarbaz", "Baz", -1, -1) + for _, dir := range []bool{true, false} { + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1) + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9) + assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1) + } } func TestEmptyPattern(t *testing.T) { - assertMatch(t, FuzzyMatch, true, "foobar", "", 0, 0) - assertMatch(t, ExactMatchNaive, true, "foobar", "", 0, 0) - assertMatch(t, PrefixMatch, true, "foobar", "", 0, 0) - assertMatch(t, SuffixMatch, true, "foobar", "", 6, 6) + for _, dir := range []bool{true, false} { + assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0) + assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0) + assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0) + assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6) + } } diff --git a/src/core.go b/src/core.go index 4f07215..96bfdd4 100644 --- a/src/core.go +++ b/src/core.go @@ -143,7 +143,8 @@ func Run(opts *Options) { // Matcher patternBuilder := func(runes []rune) *Pattern { return BuildPattern( - opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) + opts.Mode, opts.Case, opts.Tiebreak != byEnd, + opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) diff --git a/src/pattern.go b/src/pattern.go index cfeb68d..5466b86 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -39,12 +39,13 @@ type term struct { type Pattern struct { mode Mode caseSensitive bool + forward bool text []rune terms []term hasInvTerm bool delimiter Delimiter nth []Range - procFun map[termType]func(bool, []rune, []rune) (int, int) + procFun map[termType]func(bool, bool, []rune, []rune) (int, int) } var ( @@ -70,7 +71,7 @@ func clearChunkCache() { } // BuildPattern builds Pattern object from the given arguments -func BuildPattern(mode Mode, caseMode Case, +func BuildPattern(mode Mode, caseMode Case, forward bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { var asString string @@ -109,12 +110,13 @@ func BuildPattern(mode Mode, caseMode Case, ptr := &Pattern{ mode: mode, caseSensitive: caseSensitive, + forward: forward, text: []rune(asString), terms: terms, hasInvTerm: hasInvTerm, nth: nth, delimiter: delimiter, - procFun: make(map[termType]func(bool, []rune, []rune) (int, int))} + procFun: make(map[termType]func(bool, bool, []rune, []rune) (int, int))} ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termEqual] = algo.EqualMatch @@ -288,7 +290,7 @@ func dupItem(item *Item, offsets []Offset) *Item { func (p *Pattern) fuzzyMatch(item *Item) (int, int) { input := p.prepareInput(item) - return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.text) + return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) } func (p *Pattern) extendedMatch(item *Item) []Offset { @@ -296,7 +298,7 @@ func (p *Pattern) extendedMatch(item *Item) []Offset { offsets := []Offset{} for _, term := range p.terms { pfun := p.procFun[term.typ] - if sidx, eidx := p.iter(pfun, input, term.caseSensitive, term.text); sidx >= 0 { + if sidx, eidx := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 { if term.inv { break } @@ -324,11 +326,11 @@ func (p *Pattern) prepareInput(item *Item) []Token { return ret } -func (p *Pattern) iter(pfun func(bool, []rune, []rune) (int, int), - tokens []Token, caseSensitive bool, pattern []rune) (int, int) { +func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) (int, int), + tokens []Token, caseSensitive bool, forward bool, pattern []rune) (int, int) { for _, part := range tokens { prefixLength := part.prefixLength - if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 { + if sidx, eidx := pfun(caseSensitive, forward, part.text, pattern); sidx >= 0 { return sidx + prefixLength, eidx + prefixLength } } diff --git a/src/pattern_test.go b/src/pattern_test.go index 66f5d41..d508612 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -58,10 +58,10 @@ func TestParseTermsEmpty(t *testing.T) { func TestExact(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(ModeExtended, CaseSmart, + pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("'abc")) sidx, eidx := algo.ExactMatchNaive( - pattern.caseSensitive, []rune("aabbcc abc"), pattern.terms[0].text) + pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.terms[0].text) if sidx != 7 || eidx != 10 { t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) } @@ -70,11 +70,11 @@ func TestExact(t *testing.T) { func TestEqual(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, Delimiter{}, []rune("^AbC$")) + pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { sidx, eidx := algo.EqualMatch( - pattern.caseSensitive, []rune(str), pattern.terms[0].text) + pattern.caseSensitive, pattern.forward, []rune(str), pattern.terms[0].text) if sidx != sidxExpected || eidx != eidxExpected { t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) } @@ -86,17 +86,17 @@ func TestEqual(t *testing.T) { func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() - pat1 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, Delimiter{}, []rune("abc")) + pat1 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat2 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, Delimiter{}, []rune("Abc")) + pat2 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat3 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, Delimiter{}, []rune("abc")) + pat3 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat4 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, Delimiter{}, []rune("Abc")) + pat4 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat5 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, Delimiter{}, []rune("abc")) + pat5 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat6 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, Delimiter{}, []rune("Abc")) + pat6 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || @@ -109,7 +109,7 @@ func TestCaseSensitivity(t *testing.T) { } func TestOrigTextAndTransformed(t *testing.T) { - pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, Delimiter{}, []rune("jg")) + pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) tokens := Tokenize([]rune("junegunn"), Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) diff --git a/test/test_go.rb b/test/test_go.rb index 377af22..d1f45dc 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -527,6 +527,17 @@ class TestGoFZF < TestBase assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/) end + def test_tiebreak_end_backward_scan + input = %w[ + foobar-fb + fubar + ] + writelines tempname, input + + assert_equal input.reverse, `cat #{tempname} | #{FZF} -f fb`.split($/) + assert_equal input, `cat #{tempname} | #{FZF} -f fb --tiebreak=end`.split($/) + end + def test_invalid_cache tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter tmux.until { |lines| lines[-2].include? '2/3' } From de829c09385c7db074001467873620117e20237a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 12 Sep 2015 12:50:32 +0900 Subject: [PATCH 401/408] 0.10.5 --- CHANGELOG.md | 6 ++++++ install | 4 ++-- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 633d264..a337b10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +0.10.5 +------ + +- `'`-prefix to unquote the term in `--extended-exact` mode +- Backward scan when `--tiebreak=end` is set + 0.10.4 ------ diff --git a/install b/install index b04d4d0..9b9b04d 100755 --- a/install +++ b/install @@ -1,7 +1,7 @@ #!/usr/bin/env bash -[[ "$@" =~ --pre ]] && version=0.10.4 pre=1 || - version=0.10.4 pre=0 +[[ "$@" =~ --pre ]] && version=0.10.5 pre=1 || + version=0.10.5 pre=0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index d4ec1e7..0b159cc 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Aug 2015" "fzf 0.10.4" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Sep 2015" "fzf 0.10.5" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index e335a57..b2225f1 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.10.4" + version = "0.10.5" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From c656cfbdce00f04391931e66a59106ad8a12dd82 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 12 Sep 2015 13:31:07 +0900 Subject: [PATCH 402/408] Update doc --- README.md | 6 +++--- man/man1/fzf.1 | 33 ++++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6ec5fbc..614f7b0 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,9 @@ such as: `^music .mp3$ sbtrkt !rmx` | `'wild` | Items that include `wild` | exact-match (quoted) | | `!'fire` | Items that do not include `fire` | inverse-exact-match | -If you don't need fuzzy matching and do not wish to "quote" every word, start -fzf with `-e` or `--extended-exact` option. Note that in `--extended-exact` -mode, `'`-prefix "unquotes" the term. +If you don't prefer fuzzy matching and do not wish to "quote" every word, +start fzf with `-e` or `--extended-exact` option. Note that in +`--extended-exact` mode, `'`-prefix "unquotes" the term. #### Environment variables diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 0b159cc..79df436 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -374,29 +374,40 @@ mode". In this mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR .SS Exact-match (quoted) -A term that is prefixed by a single-quote character (') is interpreted as an -"exact-match" (or "non-fuzzy") term. fzf will search for the exact occurrences -of the string. +A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as +an "exact-match" (or "non-fuzzy") term. fzf will search for the exact +occurrences of the string. .SS Anchored-match -A term can be prefixed by ^, or suffixed by $ to become an anchored-match term. -Then fzf will search for the items that start with or end with the given -string. An anchored-match term is also an exact-match term. +A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an +anchored-match term. Then fzf will search for the items that start with or end +with the given string. An anchored-match term is also an exact-match term. .SS Negation -If a term is prefixed by !, fzf will exclude the items that satisfy the term -from the result. +If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the +term from the result. .SS Extended-exact mode -If you don't need fuzzy matching at all and do not wish to "quote" (prefixing -with ') every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option -(instead of \fB-x\fR or \fB--extended\fR). +If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with +\fB'\fR) every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option +(instead of \fB-x\fR or \fB--extended\fR). Note that in \fB--extended-exact\fR +mode, \fB'\fR-prefix "unquotes" the term. .SH AUTHOR Junegunn Choi (\fIjunegunn.c@gmail.com\fR) .SH SEE ALSO +.B Project homepage: +.RS .I https://github.com/junegunn/fzf +.RE +.br +.R "" +.br +.B Extra Vim plugin: +.RS +.I https://github.com/junegunn/fzf.vim +.RE .SH LICENSE MIT From fa2f9f1f21bb41ac915a564fbf45b1bf50e40546 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 14 Sep 2015 15:48:44 +0900 Subject: [PATCH 403/408] Remove flattr badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 614f7b0..956dd1e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -fzf - a command-line fuzzy finder [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) Flattr this +fzf - a command-line fuzzy finder [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) === fzf is a general-purpose command-line fuzzy finder. From 65d9d416b4300e85304fd158d9df2f6272590849 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 15 Sep 2015 13:21:51 +0900 Subject: [PATCH 404/408] Change exit status (0: OK, 1: No match, 2: Error/Interrupted) A la grep. Close #345 --- src/constants.go | 6 ++++++ src/core.go | 15 ++++++++++++--- src/curses/curses.go | 4 ++-- src/options.go | 8 ++++---- src/terminal.go | 17 ++++++++++------ test/test_go.rb | 46 +++++++++++++++++++++++++++++++++++++++----- 6 files changed, 76 insertions(+), 20 deletions(-) diff --git a/src/constants.go b/src/constants.go index b2225f1..9a4fc29 100644 --- a/src/constants.go +++ b/src/constants.go @@ -47,3 +47,9 @@ const ( EvtHeader EvtClose ) + +const ( + exitOk = 0 + exitNoMatch = 1 + exitError = 2 +) diff --git a/src/core.go b/src/core.go index 96bfdd4..04b6eab 100644 --- a/src/core.go +++ b/src/core.go @@ -56,7 +56,7 @@ func Run(opts *Options) { if opts.Version { fmt.Println(version) - os.Exit(0) + os.Exit(exitOk) } // Event channel @@ -156,12 +156,14 @@ func Run(opts *Options) { pattern := patternBuilder([]rune(*opts.Filter)) + found := false if streamingFilter { reader := Reader{ func(runes []byte) bool { item := chunkList.trans(runes, 0) if item != nil && pattern.MatchItem(item) { fmt.Println(string(item.text)) + found = true } return false }, eventBox, opts.ReadZero} @@ -176,9 +178,13 @@ func Run(opts *Options) { pattern: pattern}) for i := 0; i < merger.Length(); i++ { fmt.Println(merger.Get(i).AsString(opts.Ansi)) + found = true } } - os.Exit(0) + if found { + os.Exit(exitOk) + } + os.Exit(exitNoMatch) } // Synchronous search @@ -253,7 +259,10 @@ func Run(opts *Options) { for i := 0; i < count; i++ { fmt.Println(val.Get(i).AsString(opts.Ansi)) } - os.Exit(0) + if count > 0 { + os.Exit(exitOk) + } + os.Exit(exitNoMatch) } deferred = false terminal.startChan <- true diff --git a/src/curses/curses.go b/src/curses/curses.go index 3de8e98..59cea3b 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -261,7 +261,7 @@ func Init(theme *ColorTheme, black bool, mouse bool) { _screen = C.newterm(nil, C.stderr, C.stdin) if _screen == nil { fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) - os.Exit(1) + os.Exit(2) } C.set_term(_screen) if mouse { @@ -275,7 +275,7 @@ func Init(theme *ColorTheme, black bool, mouse bool) { go func() { <-intChan Close() - os.Exit(1) + os.Exit(2) }() if theme != nil { diff --git a/src/options.go b/src/options.go index 7090066..47d8bb1 100644 --- a/src/options.go +++ b/src/options.go @@ -180,14 +180,14 @@ func defaultOptions() *Options { Version: false} } -func help(ok int) { +func help(code int) { os.Stderr.WriteString(usage) - os.Exit(ok) + os.Exit(code) } func errorExit(msg string) { os.Stderr.WriteString(msg + "\n") - os.Exit(1) + os.Exit(exitError) } func optString(arg string, prefixes ...string) (bool, string) { @@ -682,7 +682,7 @@ func parseOptions(opts *Options, allArgs []string) { arg := allArgs[i] switch arg { case "-h", "--help": - help(0) + help(exitOk) case "-x", "--extended": opts.Mode = ModeExtended case "-e", "--extended-exact": diff --git a/src/terminal.go b/src/terminal.go index 053ed78..c3fb966 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -280,17 +280,19 @@ func (t *Terminal) UpdateList(merger *Merger) { t.reqBox.Set(reqList, nil) } -func (t *Terminal) output() { +func (t *Terminal) output() bool { if t.printQuery { fmt.Println(string(t.input)) } if len(t.expect) > 0 { fmt.Println(t.pressed) } - if len(t.selected) == 0 { + found := len(t.selected) > 0 + if !found { cnt := t.merger.Length() if cnt > 0 && cnt > t.cy { fmt.Println(t.merger.Get(t.cy).AsString(t.ansi)) + found = true } } else { sels := make([]selectedItem, 0, len(t.selected)) @@ -302,6 +304,7 @@ func (t *Terminal) output() { fmt.Println(*sel.text) } } + return found } func runeWidth(r rune, prefixWidth int) int { @@ -743,7 +746,7 @@ func (t *Terminal) Loop() { } exit := func(code int) { - if code == 0 && t.history != nil { + if code <= exitNoMatch && t.history != nil { t.history.append(string(t.input)) } os.Exit(code) @@ -776,11 +779,13 @@ func (t *Terminal) Loop() { t.printAll() case reqClose: C.Close() - t.output() - exit(0) + if t.output() { + exit(exitOk) + } + exit(exitNoMatch) case reqQuit: C.Close() - exit(1) + exit(exitError) } } t.placeCursor() diff --git a/test/test_go.rb b/test/test_go.rb index d1f45dc..5b35264 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -780,11 +780,6 @@ class TestGoFZF < TestBase tmux.send_keys :Enter end - def test_invalid_term - tmux.send_keys "TERM=xxx fzf", :Enter - tmux.until { |lines| lines.any? { |l| l.include? 'Invalid $TERM: xxx' } } - end - def test_with_nth writelines tempname, ['hello world ', 'byebye'] assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp @@ -801,6 +796,47 @@ class TestGoFZF < TestBase assert_equal src, `cat #{tempname} | #{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi`.chomp end + def test_exit_0_exit_code + `echo foo | #{FZF} -q bar -0` + assert_equal 1, $?.exitstatus + end + + def test_invalid_term + lines = `TERM=xxx #{FZF}` + assert_equal 2, $?.exitstatus + assert lines.include?('Invalid $TERM: xxx') + end + + def test_invalid_option + lines = `#{FZF} --foobar 2>&1` + assert_equal 2, $?.exitstatus + assert lines.include?('unknown option: --foobar'), lines + end + + def test_filter_exitstatus + # filter / streaming filter + ["", "--no-sort"].each do |opts| + assert `echo foo | #{FZF} -f foo #{opts}`.include?('foo') + assert_equal 0, $?.exitstatus + + assert `echo foo | #{FZF} -f bar #{opts}`.empty? + assert_equal 1, $?.exitstatus + end + end + + def test_exitstatus_empty + { '99' => '0', '999' => '1' }.each do |query, status| + tmux.send_keys "seq 100 | #{FZF} -q #{query}", :Enter + tmux.until { |lines| lines[-2] =~ %r{ [10]/100} } + tmux.send_keys :Enter + + tmux.send_keys 'echo --\$?--' + tmux.until { |lines| lines.last.include? "echo --$?--" } + tmux.send_keys :Enter + tmux.until { |lines| lines.last.include? "--#{status}--" } + end + end + private def writelines path, lines File.unlink path while File.exists? path From 2022a3ad96f027e056e4fcce11fee0976db657d1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 15 Sep 2015 19:04:53 +0900 Subject: [PATCH 405/408] Replace --header-file with --header (#346) and allow using --header and --header-lines at the same time. Close #346. --- CHANGELOG.md | 10 ++++++++++ man/man1/fzf.1 | 10 +++++----- shell/completion.bash | 4 ++-- src/core.go | 2 +- src/options.go | 26 ++++++++------------------ src/terminal.go | 36 +++++++++++++++++++++--------------- test/test_go.rb | 28 ++++++++++++++++++++++++---- 7 files changed, 71 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a337b10..f10cbca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ CHANGELOG ========= +0.10.6 +------ + +- Replaced `--header-file` with `--header` option +- `--header` and `--header-lines` can be used together +- Changed exit status + - 0: Okay + - 1: No match + - 2: Error/Interrupted + 0.10.5 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 79df436..4147371 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -285,11 +285,11 @@ When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to Maximum number of entries in the history file (default: 1000). The file is automatically truncated when the number of the lines exceeds the value. .TP -.BI "--header-file=" "FILE" -The content of the file will be printed as the sticky header. The lines in the -file are displayed in order from top to bottom regardless of \fB--reverse\fR, -and are not affected by \fB--with-nth\fR. ANSI color codes are processed even -when \fB--ansi\fR is not set. +.BI "--header=" "STR" +The given string will be printed as the sticky header. The lines are displayed +in the given order from top to bottom regardless of \fB--reverse\fR option, and +are not affected by \fB--with-nth\fR. ANSI color codes are processed even when +\fB--ansi\fR is not set. .TP .BI "--header-lines=" "N" The first N lines of the input are treated as the sticky header. When diff --git a/shell/completion.bash b/shell/completion.bash index 63de546..d7fdf26 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -49,7 +49,7 @@ _fzf_opts_completion() { --cycle --history --history-size - --header-file + --header --header-lines --margin" @@ -62,7 +62,7 @@ _fzf_opts_completion() { COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) ) return 0 ;; - --history|--header-file) + --history) COMPREPLY=() return 0 ;; diff --git a/src/core.go b/src/core.go index 04b6eab..35d7ced 100644 --- a/src/core.go +++ b/src/core.go @@ -238,7 +238,7 @@ func Run(opts *Options) { } case EvtHeader: - terminal.UpdateHeader(value.([]string), opts.HeaderLines) + terminal.UpdateHeader(value.([]string)) case EvtSearchFin: switch val := value.(type) { diff --git a/src/options.go b/src/options.go index 47d8bb1..9d8aaa1 100644 --- a/src/options.go +++ b/src/options.go @@ -1,7 +1,6 @@ package fzf import ( - "io/ioutil" "os" "regexp" "strconv" @@ -45,7 +44,7 @@ const usage = `usage: fzf [options] --bind=KEYBINDS Custom key bindings. Refer to the man page. --history=FILE History file --history-size=N Maximum number of history entries (default: 1000) - --header-file=FILE The file whose content to be printed as header + --header=STR String to print as header --header-lines=N The first N lines of the input are treated as header Scripting @@ -604,12 +603,8 @@ func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType { return keymap } -func readHeaderFile(filename string) []string { - content, err := ioutil.ReadFile(filename) - if err != nil { - errorExit("failed to read header file: " + filename) - } - return strings.Split(strings.TrimSuffix(string(content), "\n"), "\n") +func strLines(str string) []string { + return strings.Split(strings.TrimSuffix(str, "\n"), "\n") } func parseMargin(margin string) [4]string { @@ -793,16 +788,13 @@ func parseOptions(opts *Options, allArgs []string) { setHistory(nextString(allArgs, &i, "history file path required")) case "--history-size": setHistoryMax(nextInt(allArgs, &i, "history max size required")) - case "--no-header-file": + case "--no-header": opts.Header = []string{} case "--no-header-lines": opts.HeaderLines = 0 - case "--header-file": - opts.Header = readHeaderFile( - nextString(allArgs, &i, "header file name required")) - opts.HeaderLines = 0 + case "--header": + opts.Header = strLines(nextString(allArgs, &i, "header string required")) case "--header-lines": - opts.Header = []string{} opts.HeaderLines = atoi( nextString(allArgs, &i, "number of header lines required")) case "--no-margin": @@ -843,11 +835,9 @@ func parseOptions(opts *Options, allArgs []string) { setHistory(value) } else if match, value := optString(arg, "--history-size="); match { setHistoryMax(atoi(value)) - } else if match, value := optString(arg, "--header-file="); match { - opts.Header = readHeaderFile(value) - opts.HeaderLines = 0 + } else if match, value := optString(arg, "--header="); match { + opts.Header = strLines(value) } else if match, value := optString(arg, "--header-lines="); match { - opts.Header = []string{} opts.HeaderLines = atoi(value) } else if match, value := optString(arg, "--margin="); match { opts.Margin = parseMargin(value) diff --git a/src/terminal.go b/src/terminal.go index c3fb966..5e8300f 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -42,6 +42,7 @@ type Terminal struct { history *History cycle bool header []string + header0 []string ansi bool margin [4]string marginInt [4]int @@ -185,6 +186,12 @@ func defaultKeymap() map[int]actionType { // NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) + var header []string + if opts.Reverse { + header = opts.Header + } else { + header = reverseStringArray(opts.Header) + } return &Terminal{ inlineInfo: opts.InlineInfo, prompt: opts.Prompt, @@ -207,7 +214,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { margin: opts.Margin, marginInt: [4]int{0, 0, 0, 0}, cycle: opts.Cycle, - header: opts.Header, + header: header, + header0: header, ansi: opts.Ansi, reading: true, merger: EmptyMerger, @@ -241,18 +249,19 @@ func (t *Terminal) UpdateCount(cnt int, final bool) { } } -// UpdateHeader updates the header -func (t *Terminal) UpdateHeader(header []string, lines int) { - t.mutex.Lock() - t.header = make([]string, lines) - copy(t.header, header) - if !t.reverse { - reversed := make([]string, lines) - for idx, str := range t.header { - reversed[lines-idx-1] = str - } - t.header = reversed +func reverseStringArray(input []string) []string { + size := len(input) + reversed := make([]string, size) + for idx, str := range input { + reversed[size-idx-1] = str } + return reversed +} + +// UpdateHeader updates the header +func (t *Terminal) UpdateHeader(header []string) { + t.mutex.Lock() + t.header = append(append([]string{}, t.header0...), header...) t.mutex.Unlock() t.reqBox.Set(reqHeader, nil) } @@ -436,9 +445,6 @@ func (t *Terminal) printHeader() { max := t.maxHeight() var state *ansiState for idx, lineStr := range t.header { - if !t.reverse { - idx = len(t.header) - idx - 1 - } line := idx + 2 if t.inlineInfo { line-- diff --git a/test/test_go.rb b/test/test_go.rb index 5b35264..e76b520 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -737,8 +737,8 @@ class TestGoFZF < TestBase assert_equal '6', readonce.chomp end - def test_header_file - tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__})"}", :Enter + def test_header + tmux.send_keys "seq 100 | #{fzf "--header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter header = File.readlines(__FILE__).take(5).map(&:strip) tmux.until do |lines| lines[-2].include?('100/100') && @@ -746,8 +746,8 @@ class TestGoFZF < TestBase end end - def test_header_file_reverse - tmux.send_keys "seq 100 | #{fzf "--header-file=<(head -5 #{__FILE__}) --reverse"}", :Enter + def test_header_reverse + tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(head -5 #{__FILE__})\\\" --reverse"}", :Enter header = File.readlines(__FILE__).take(5).map(&:strip) tmux.until do |lines| lines[1].include?('100/100') && @@ -755,6 +755,26 @@ class TestGoFZF < TestBase end end + def test_header_and_header_lines + tmux.send_keys "seq 100 | #{fzf "--header-lines 10 --header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter + header = File.readlines(__FILE__).take(5).map(&:strip) + tmux.until do |lines| + lines[-2].include?('90/90') && + lines[-7...-2].map(&:strip) == header && + lines[-17...-7].map(&:strip) == (1..10).map(&:to_s).reverse + end + end + + def test_header_and_header_lines_reverse + tmux.send_keys "seq 100 | #{fzf "--reverse --header-lines 10 --header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter + header = File.readlines(__FILE__).take(5).map(&:strip) + tmux.until do |lines| + lines[1].include?('90/90') && + lines[2...7].map(&:strip) == header && + lines[7...17].map(&:strip) == (1..10).map(&:to_s) + end + end + def test_canel tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter tmux.until { |lines| lines[-2].include?('10/10') } From 8764be07e2c97b667bd8437417e0a592082a918d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 18 Sep 2015 09:59:40 +0900 Subject: [PATCH 406/408] [vim] Ignore exit status of 2 (#345) --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index eea1ffd..e6aecf4 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -229,7 +229,7 @@ function! s:execute(dict, command, temps) redraw! if v:shell_error " Do not print error message on exit status 1 - if v:shell_error > 1 + if v:shell_error > 2 echohl ErrorMsg echo 'Error running ' . command endif From ca19762e58533c6d7c7b25d7fcc41dc7ea898b3e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 18 Sep 2015 10:25:07 +0900 Subject: [PATCH 407/408] Exit status 130 when fzf is terminated by the user Related: #345 --- plugin/fzf.vim | 5 +++-- src/constants.go | 7 ++++--- src/terminal.go | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index e6aecf4..d9ced8e 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -228,10 +228,11 @@ function! s:execute(dict, command, temps) execute 'silent !'.command redraw! if v:shell_error - " Do not print error message on exit status 1 - if v:shell_error > 2 + " Do not print error message on exit status 1 (no match) or 130 (interrupt) + if v:shell_error == 2 echohl ErrorMsg echo 'Error running ' . command + echohl None endif return [] else diff --git a/src/constants.go b/src/constants.go index 9a4fc29..1cc4f81 100644 --- a/src/constants.go +++ b/src/constants.go @@ -49,7 +49,8 @@ const ( ) const ( - exitOk = 0 - exitNoMatch = 1 - exitError = 2 + exitOk = 0 + exitNoMatch = 1 + exitError = 2 + exitInterrupt = 130 ) diff --git a/src/terminal.go b/src/terminal.go index 5e8300f..9a92ba4 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -791,7 +791,7 @@ func (t *Terminal) Loop() { exit(exitNoMatch) case reqQuit: C.Close() - exit(exitError) + exit(exitInterrupt) } } t.placeCursor() From 93988780484c9e77dc468ed7e1b9be05c7335f7d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 18 Sep 2015 10:26:04 +0900 Subject: [PATCH 408/408] [fzf-tmux] Exit with the same exit status as with fzf --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 5f690e5..239c878 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -102,7 +102,7 @@ trap cleanup EXIT SIGINT SIGTERM fail() { >&2 echo "$1" - exit 1 + exit 2 } fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" [ -x "$fzf" ] || fail "fzf executable not found" @@ -134,5 +134,5 @@ else cat <&0 > $fifo1 & fi cat $fifo2 -[ "$(cat $fifo3)" = '0' ] +exit "$(cat $fifo3)"